【问题标题】:Most efficient way of making an if-elif-elif-else statement when the else is done the most?当 else 完成得最多时,制作 if-elif-elif-else 语句的最有效方法是什么?
【发布时间】:2013-06-14 12:03:05
【问题描述】:

我有一个 in if-elif-elif-else 语句,其中 99% 的时间执行 else 语句:

if something == 'this':
    doThis()
elif something == 'that':
    doThat()
elif something == 'there':
    doThere()
else:
    doThisMostOfTheTime()

这个构造做了很多,但是由于它在遇到 else 之前遍历了所有条件,我觉得这不是很有效,更不用说 Pythonic 了。另一方面,它确实需要知道是否满足这些条件中的任何一个,因此无论如何它都应该对其进行测试。

有谁知道这是否以及如何更有效地完成,或者这仅仅是最好的方法吗?

【问题讨论】:

  • 你能sort 运行你的 if/else... 链的东西吗,这样一个条件匹配的所有元素都在一端,其余的都是在另一个?如果是这样,您可以看看这是否更快/更优雅。但请记住,如果没有性能问题,那么担心优化还为时过早。
  • 这三种特殊情况有什么共同点吗?例如,您可以做if not something.startswith("th"): doThisMostOfTheTime() 并在else 子句中做另一个比较。
  • @kramer65 如果是这么长的 if/elif 链......它可能会很慢,但请确保真正profile您的代码并从优化任何部分开始大部分时间。
  • 这些比较是对something 的每个值执行一次,还是对同一个值执行多次类似的比较?

标签: python performance if-statement


【解决方案1】:

代码...

options.get(something, doThisMostOfTheTime)()

...看起来它应该更快,但它实际上比 if ... elif ... else 构造慢,因为它必须调用一个函数,这可能是一个重要的紧密循环中的性能开销。

考虑这些例子...

1.py

something = 'something'

for i in xrange(1000000):
    if something == 'this':
        the_thing = 1
    elif something == 'that':
        the_thing = 2
    elif something == 'there':
        the_thing = 3
    else:
        the_thing = 4

2.py

something = 'something'
options = {'this': 1, 'that': 2, 'there': 3}

for i in xrange(1000000):
    the_thing = options.get(something, 4)

3.py

something = 'something'
options = {'this': 1, 'that': 2, 'there': 3}

for i in xrange(1000000):
    if something in options:
        the_thing = options[something]
    else:
        the_thing = 4

4.py

from collections import defaultdict

something = 'something'
options = defaultdict(lambda: 4, {'this': 1, 'that': 2, 'there': 3})

for i in xrange(1000000):
    the_thing = options[something]

...并注意他们使用的 CPU 时间...

1.py: 160ms
2.py: 170ms
3.py: 110ms
4.py: 100ms

...使用来自time(1) 的用户时间。

选项 #4 确实有额外的内存开销,即为每个不同的键未命中添加一个新项目,因此,如果您期望不同的键未命中数不限,我会选择选项 #3,它仍然是对原始结构的显着改进。

【讨论】:

  • python有switch语句吗?
  • 呃...到目前为止,这是我听说的关于 python 的唯一一件我不关心的事情......我猜肯定会有什么事情
  • -1 你说使用dict 比较慢,但是你的时间实际上表明它是第二快的选项。
  • @Marcin 我是说dict.get() 更慢,它是2.py——它们中最慢的。
  • 3 和 4 更快,因为使用 ìf x in ...` 执行字典查找比函数调用(即 dict.get('blah', None))快得多,实际上 set() 查找甚至比dict的查找
【解决方案2】:

我会创建一个字典:

options = {'this': doThis,'that' :doThat, 'there':doThere}

现在只使用:

options.get(something, doThisMostOfTheTime)()

如果在options 字典中找不到something,则dict.get 将返回默认值doThisMostOfTheTime

一些时间比较:

脚本:

from random import shuffle
def doThis():pass
def doThat():pass
def doThere():pass
def doSomethingElse():pass
options = {'this':doThis, 'that':doThat, 'there':doThere}
lis = range(10**4) + options.keys()*100
shuffle(lis)

def get():
    for x in lis:
        options.get(x, doSomethingElse)()

def key_in_dic():
    for x in lis:
        if x in options:
            options[x]()
        else:
            doSomethingElse()

def if_else():
    for x in lis:
        if x == 'this':
            doThis()
        elif x == 'that':
            doThat()
        elif x == 'there':
            doThere()
        else:
            doSomethingElse()

结果:

>>> from so import *
>>> %timeit get()
100 loops, best of 3: 5.06 ms per loop
>>> %timeit key_in_dic()
100 loops, best of 3: 3.55 ms per loop
>>> %timeit if_else()
100 loops, best of 3: 6.42 ms per loop

对于10**5不存在的密钥和100个有效密钥::

>>> %timeit get()
10 loops, best of 3: 84.4 ms per loop
>>> %timeit key_in_dic()
10 loops, best of 3: 50.4 ms per loop
>>> %timeit if_else()
10 loops, best of 3: 104 ms per loop

因此,对于普通字典来说,使用 key in options 检查键是这里最有效的方法:

if key in options:
   options[key]()
else:
   doSomethingElse()

【讨论】:

  • options = collections.defaultdict(lambda: doThisMostOfTheTime, {'this': doThis,'that' :doThat, 'there':doThere}); options[something]() 效率略高。
  • 好主意,但不那么可读。此外,您可能希望分离 options dict 以避免重建它,从而将部分(但不是全部)逻辑远离使用点。不过,不错的技巧!
  • 知道这是否更有效?我的猜测是它更慢,因为它正在执行哈希查找而不是简单的条件检查或三个。问题是关于效率而不是代码的紧凑性。
  • @BryanOakley 我添加了一些时间比较。
  • 实际上使用try: options[key]() except KeyError: doSomeThingElse() 应该更有效(因为使用if key in options: options[key](),您需要在字典中搜索两次key
【解决方案3】:

你会用pypy吗?

保留您的原始代码但在 pypy 上运行它可以使我的速度提高 50 倍。

CPython:

matt$ python
Python 2.6.8 (unknown, Nov 26 2012, 10:25:03)
[GCC 4.2.1 Compatible Apple Clang 3.0 (tags/Apple/clang-211.12)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
>>> from timeit import timeit
>>> timeit("""
... if something == 'this': pass
... elif something == 'that': pass
... elif something == 'there': pass
... else: pass
... """, "something='foo'", number=10000000)
1.728302001953125

派比:

matt$ pypy
Python 2.7.3 (daf4a1b651e0, Dec 07 2012, 23:00:16)
[PyPy 2.0.0-beta1 with GCC 4.2.1] on darwin
Type "help", "copyright", "credits" or "license" for more information.
And now for something completely different: ``a 10th of forever is 1h45''
>>>>
>>>> from timeit import timeit
>>>> timeit("""
.... if something == 'this': pass
.... elif something == 'that': pass
.... elif something == 'there': pass
.... else: pass
.... """, "something='foo'", number=10000000)
0.03306388854980469

【讨论】:

  • 嗨,福兹。谢谢你的提示。事实上我已经在使用 pypy(喜欢它),但我仍然需要提高速度.. :)
  • 哦,好吧!在此之前,我尝试预先计算“this”、“that”和“there”的哈希值——然后比较哈希码而不是字符串。结果是原来的速度是原来的两倍,所以看起来字符串比较在内部已经得到了很好的优化。
【解决方案4】:

这里是一个将动态条件转换为字典的 if 示例。

selector = {lambda d: datetime(2014, 12, 31) >= d : 'before2015',
            lambda d: datetime(2015, 1, 1) <= d < datetime(2016, 1, 1): 'year2015',
            lambda d: datetime(2016, 1, 1) <= d < datetime(2016, 12, 31): 'year2016'}

def select_by_date(date, selector=selector):
    selected = [selector[x] for x in selector if x(date)] or ['after2016']
    return selected[0]

这是一种方式,但可能不是最 Pythonic 的方式,因为对于不熟悉 Python 的人来说可读性较差。

【讨论】:

    【解决方案5】:

    我尝试了在 python 3.10 中引入的 match 语句:

    5.py

    ​​>
    something = 'something'
    for i in range(10000000):
        match something:
            case "this":
                the_thing = 1
            case "that":
                the_thing = 2
            case "there":
                the_thing = 3
            case _:
                the_thing = 4
    

    以下是我使用 3.10.0 得到的结果:
    1.py:1.4s
    2.py:0.9s
    3.py:0.7s
    4.py:0.7s
    5.py:1.0s
    我以为我会得到类似于 1.py 的东西,但它要快得多。

    【讨论】:

      【解决方案6】:

      出于安全原因,人们警告exec,但这是一个理想的情况。
      这是一个简单的状态机。

      Codes = {}
      Codes [0] = compile('blah blah 0; nextcode = 1')
      Codes [1] = compile('blah blah 1; nextcode = 2')
      Codes [2] = compile('blah blah 2; nextcode = 0')
      
      nextcode = 0
      While True:
          exec(Codes[nextcode])
      

      【讨论】:

        【解决方案7】:

        最近我遇到了一种替代“嵌套 if else”的方法,它将我的函数的运行时间从 2.5 小时减少到 ~2 分钟..Baam!让我们开始吧:

        早期代码
        bin = lambda x:"Unknown" if x==0 else("High" if x>75 else("Medium" if x>50 and x25 and x

        col.apply(bin) 时间~2.5小时

        优化代码

        定义字典替代嵌套 if else
         def dict_function(*args):
        'Pass in a list of tuples, which will be key/value pairs'
        ret = {}
        for k,v in args:
            for i in k:
                ret[i] = v
        return ret
        Dict = dict_function(([0],"Unknown"),(range(1,25),"Low"),(range(25,50),"Medium_Low"),(range(50,75),"Medium"),(range(75,100),"High"))
        
        col.apply(lambda x:Dict[x])
        

        dict_function 为给定范围创建多个 key_value 对。 时间~2分钟

        【讨论】:

          【解决方案8】:

          我最近遇到了同样的问题,虽然不是关于性能,但我不喜欢创建函数并将它们手动添加到字典的“API”。我想要一个类似于functools.singledispatch 的 API,但基于值而不是类型进行调度。所以...

          def value_dispatch(func):
              """value-dispatch function decorator.
              Transforms a function into a function, that dispatches its calls based on the
              value of the first argument.
              """
              funcname = getattr(func, '__name__')
              registry = {}
          
              def dispatch(arg):
                  """return the function that matches the argument"""
                  return registry.get(arg, func)
          
              def register(arg):
                  def wrapper(func):
                      """register a function"""
                      registry[arg] = func
                      return func
                  return wrapper
          
              def wrapper(*args, **kwargs):
                  if not args:
                      raise ValueError(f'{funcname} requires at least 1 positional argument')
                  return dispatch(args[0])(*args, **kwargs)
          
              wrapper.register = register
              wrapper.dispatch = dispatch
              wrapper.registry = registry
              return wrapper
          

          这样使用:

          @value_dispatch
          def handle_something():
              print("default")
          
          @handle_something.register(1)
          def handle_one():
              print("one")
          
          handle_something(1)
          handle_something(2)
          

          PS:我创建了a snippet on Gitlab供参考

          【讨论】:

            【解决方案9】:

            你可以用 switch-case 类型模仿 if-elif-else,比如使用字典和 lambda 函数

            例如:

            x = 5
            y = 5
            operator = 'add'
            
            def operation(operator, x, y): 
             return {
               'add': lambda: x+y,
               'sub': lambda: x-y,
               'mul': lambda: x*y,
               'div': lambda: x/y
             }.get(operator, lambda: None)()
            
            result = operation(operator, x, y)
            print(result)
            

            【讨论】:

              猜你喜欢
              • 2017-07-03
              • 1970-01-01
              • 2022-11-13
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2017-08-10
              相关资源
              最近更新 更多