关于python:当else被做得最多时,最有效的if-elif-elif-else语句的方法是什么?

Most efficient way of making an if-elif-elif-else statement when the else is done the most?

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

1
2
3
4
5
6
7
8
if something == 'this':
    doThis()
elif something == 'that':
    doThat()
elif something == 'there':
    doThere()
else:
    doThisMostOfTheTime()

这个构造做了很多,但是因为它在碰到其他东西之前就已经克服了所有的条件,所以我觉得这不是很有效,更不用说Python了。另一方面,它需要知道是否满足这些条件中的任何一个,因此它无论如何都应该测试它。

有人知道这是否以及如何能更有效地做到,或者这仅仅是最好的方法吗?


代码…

1
options.get(something, doThisMostOfTheTime)()

…看起来应该更快,但实际上比if慢……elifelse构造,因为它必须调用一个函数,这在一个紧凑的循环中可能是一个很大的性能开销。

考虑这些例子…

1.Py

1
2
3
4
5
6
7
8
9
10
11
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

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

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

3.Py

1
2
3
4
5
6
7
8
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

1
2
3
4
5
6
7
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
2
3
4
1.py: 160ms
2.py: 170ms
3.py: 110ms
4.py: 100ms

…使用time(1)的用户时间。

选项4确实有额外的内存开销,可以为每个不同的键未命中添加一个新项,因此,如果您希望有无限数量的不同键未命中,我将使用选项3,这仍然是对原始构造的一个显著改进。


我会创建一本字典:

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

现在只使用:

1
options.get(something, doThisMostOfTheTime)()

如果optionsdict中没有找到something,那么dict.get将返回默认值doThisMostOfTheTime

一些时间比较:

脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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()

结果:

1
2
3
4
5
6
7
>>> 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个有效密钥:

1
2
3
4
5
6
>>> %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检查密钥是最有效的方法:

1
2
3
4
if key in options:
   options[key]()
else:
   doSomethingElse()


你能用派比吗?

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

CPython:

1
2
3
4
5
6
7
8
9
10
11
12
13
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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


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

1
2
3
4
5
6
7
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的人来说,它的可读性较低。