当else做得最多时,最有效的方式来执行if-elif-elif-else语句?


99

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

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

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

是否有人知道是否以及如何可以更有效地完成此工作,或者这仅仅是做到这一点的最佳方法?


sort是否可以运行if / else ...链上的东西,这样一个条件将匹配的所有元素都在一端,而其余所有条件都在另一端?如果是这样,您可以看到它是否更快/更优雅。但是请记住,如果没有性能问题,现在担心优化还为时过早。
Patashu


4
这三种特殊情况有什么共同点吗?例如,您可以if not something.startswith("th"): doThisMostOfTheTime()else子句中进行另一个比较。
Tim Pietzcker 2013年

3
@ kramer65如果if / elif的链这么长,可能会很慢,但是请确保实际配置您的代码,并从最耗时的部分开始进行优化。
jorgeca 2013年

1
这些比较是对的每个值仅执行一次something,还是对相同的值进行多次类似的比较?
克里斯·皮特曼

Answers:


98

代码...

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,它在原始构造。


2
python是否有switch语句?
森·海菲尔德

嗯...到目前为止,这是我所听说的关于python的唯一我不关心的...猜测那里肯定有什么东西
森·海菲尔德

2
-1您说使用a dict速度较慢,但​​实际上您的计时表明它是第二快的选择。
Marcin 2013年

11
@Marcin我要说的dict.get()是慢,这是2.py-他们中最慢的。
2013年

作为记录,三个和四个也比捕获try / except构造中的关键错误要快得多。
杰夫

78

我要创建一个字典:

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

现在只使用:

options.get(something, doThisMostOfTheTime)()

如果somethingoptionsdict中找不到,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]()效率更高。
2013年

不错的主意,但不易读。另外,您可能希望将options字典分开,以避免重建它,从而将部分(而非全部)逻辑移离使用点。还是,不错的把戏!
Anders Johansson

7
知道这是否更有效吗?我的猜测是它比较慢,因为它正在执行哈希查找,而不是简单的条件检查或三个条件检查。问题是效率,而不是代码的紧凑性。
Bryan Oakley 2013年

2
@BryanOakley我添加了一些时间比较。
Ashwini Chaudhary

1
实际上,这样做应该会更有效率try: options[key]() except KeyError: doSomeThingElse()(因为if key in options: options[key]()您在字典中两次搜索了key
hardmooth,2016年

8

可以使用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

pypy:

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(喜欢它),但是我仍然需要提高速度。.:)
kramer65

那好吧!在此之前,我尝试为“ this”,“ that”和“ there”预先计算哈希值,然后比较哈希码而不是字符串。原来,它的速度是原始速度的两倍,因此看起来字符串比较已经在内部进行了很好的优化。
foz 2013年

3

这里是将动态条件转换为字典的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]

这是一种方法,但可能不是最Python的方法,因为对于不熟练使用Python的人来说可读性较差。


0

人们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])
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.