我有一个 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
子句中进行另一个比较。
something
的值执行一次,还是对同一值执行多次类似的比较?
编码...
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,这仍然是一个显着的改进原来的构造。
我会创建一个字典:
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 分开以避免重建它,从而将部分(但不是全部)逻辑远离使用点。不过,不错的把戏!
try: options[key]() except KeyError: doSomeThingElse()
应该更有效(因为使用 if key in options: options[key]()
,您在字典中搜索 key
两次
你能用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
这是一个将动态条件转换为字典的 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 的人来说可读性较差。
我尝试使用 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 但它更快。
人们出于安全原因对 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])
最近我遇到了一种替代“嵌套 if else”的方法,它将我的函数的运行时间从 2.5 小时减少到 ~2 分钟..Baam!让我们开始:
早期代码 bin = lambda x:"Unknown" if x==0 else("High" if x>75 else("Medium" if x>50 and x<=75 else("Medium_Low" if x>25 and x< =50 否则“低”)))
col.apply(bin) 时间 ~2.5hrs
优化代码
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分钟
我最近遇到了同样的问题,虽然不是关于性能,但我不喜欢创建函数并将它们手动添加到字典的“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 以供参考
你可以用 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)
dict
较慢,但您的时间实际上表明它是第二快的选项。dict.get()
较慢,即2.py
- 它们中最慢的。dict.get('blah', None)
)快得多,实际上 set() 查找甚至比 dict 的查找更快