当else完成时,制作if-elif-elif-else语句的最有效方法是什么?

时间:2013-06-18 10:05:17

标签: python performance if-statement

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

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

这个构造已经很多,但由于它在遇到其他条件之前会超过每个条件,我感觉这不是很有效,更不用说Pythonic了。另一方面,它确实需要知道是否满足任何条件,所以它应该测试它。

有人知道是否以及如何更有效地完成这项工作,或者这只是最好的方法吗?

7 个答案:

答案 0 :(得分:87)

代码......

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,它仍然是原始结构的显着改善。

答案 1 :(得分:74)

我要创建一个词典:

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

现在只使用:

options.get(something, doThisMostOfTheTime)()

如果在something字典中找不到options,则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()

答案 2 :(得分:7)

你能使用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

答案 3 :(得分:0)

这是将动态条件转换为字典的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的人来说,它的可读性较差。

答案 4 :(得分: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])

答案 5 :(得分:0)

最近,我遇到了一种替代“嵌套”的方法,该方法将我的函数的运行时间从2.5小时减少到了约2分钟。让我们开始吧:

早期代码
bin = lambda x:如果x == 0,则为“未知”;否则,如果x> 75,否则为“高”;如果x> 50,且x <= 75,则为“中等”;否则,如果x> 25和x <= 50否则为“低”)))

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

优化代码

定义字典替代嵌套,否则
 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分钟

答案 6 :(得分:0)

最近我遇到了同样的问题,尽管与性能无关,但是我不喜欢创建函数并将其手动添加到字典中的“ 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供参考