Python是否优化lambda x: x
我写了一段代码来找最大值和最小值,假设生成数据的过程比较慢(否则我就直接用 max
和 min
了),这段代码需要一个关键函数,如果没有提供,就会使用一个默认的函数:
if key is None:
key = lambda x: x
然后在后面:
for i in iterable:
key_i = key(i)
由于生成数据的过程比较慢,这个问题可能没什么意义,但如果没有提供关键函数,我会对每个项目调用 lambda x: x
。我想Python可能会优化掉这个默认函数。有人能告诉我它是否真的这样做了吗? 如果没有优化,那这个过程会消耗多少资源?有没有更好的方法来实现这个功能,而不需要增加太多代码行数(比如使用三元运算符)?
4 个回答
CPython并不会把这个优化掉。为什么呢?因为在调用之前,它并不知道这个函数是个恒等函数。
很遗憾,lambda x: x
只是创建了一个函数,从外面看我们根本不知道它在干什么。当然,从理论上讲,我们可以意识到这只是一个恒等函数,计算起来其实没什么意义。但即便如此,我们也只是把这个函数存储在一个变量里,暂时就这样吧。
然后稍后我们调用这个名字,执行底层的函数。因为这是一个函数,而我们对这个函数一无所知,所以我们只能执行它。理论上,一个优化器可以识别出这是一个恒等函数,直接返回值而跳过调用,但在Python中做到这一点会很困难。Peephole优化器在看到某些可能性时,已经会去掉一些字节码指令,但在这种情况下,这就很难实现:
调用一个名字通常是一个 LOAD_FAST
操作,接着是加载参数,然后是 CALL_FUNCTION
。这直接来自于 something(args)
的语法。所以一个理论上的优化器需要跳过第一次加载和调用函数。但要做到这一点,它必须知道最开始加载的名字指的是一个恒等函数。
现在,Peephole优化器的工作方式是,它不处理动态变量内容。即使我们能给函数加上某种标记,以便快速检查它是否是恒等函数,优化器仍然无法读取这些信息,因为它不处理底层数据。它只处理字节码操作,比如把 LOAD_GLOBAL True
简化为 LOAD_CONST True
。
说实话,为恒等函数引入这样的标记其实挺奇怪的。恒等函数本身就很少见;如果我们要优化这个,完全可以把所有的lambda函数内联,这样就能完全减少函数调用的开销。但这并不是Peephole优化器或任何其他解释型语言的优化器所做的。运行时的开销可能会太大,反而对整体性能产生负面影响,根本不值得进行这种微优化。
因为大多数情况下,这种级别的优化根本不值得。这样的函数调用很少会成为你应用的瓶颈,即使真的是,你也得考虑用其他方式来优化它。
性能检查
这里还没有人做过性能检查,所以我想分享一下我认为可能更好的选择:使用三元运算符或者分开控制流程(我觉得分开控制流程会是最好的解决方案)。
为了测试它们,我们可以在 Python 3 中这样做:
import timeit
setup = """
def control_flow(iterable, key=None):
if key is None:
for i in iterable:
pass
else:
for i in iterable:
key_i = key(i)
def identity_lambda(iterable, key=None):
if key is None:
key = lambda x: x
for i in iterable:
key_i = key(i)
def ternary(iterable, key=None):
for i in iterable:
key_i = key(i) if key else i
"""
print('Testing no lambda')
timeit.timeit('control_flow(range(100))', setup=setup)
timeit.timeit('identity_lambda(range(100))', setup=setup)
timeit.timeit('ternary(range(100))', setup=setup)
print('Testing with lambda')
timeit.timeit('control_flow(range(100), lambda x: -x)', setup=setup)
timeit.timeit('identity_lambda(range(100), lambda x: -x)', setup=setup)
timeit.timeit('ternary(range(100), lambda x: -x)', setup=setup)
这是测试结果:
Testing no lambda
1.8421741100028157
10.212458187001175
3.39080909700715
Testing with lambda
14.262093641998945
14.405747531011002
14.198169080002117
所以我认为最好的选择是分开控制流程,这样在每个分支下的代码量基本上翻倍,而不是使用 lambda x: x
,至少在这种情况下是这样。我认为这里最重要的一点是,Python 对于身份函数并没有进行优化,有时候增加代码行数反而能提高性能,尽管这样可能会导致代码不易维护和更容易出错。
好问题!一个优化器可能会发现,在某些可预测的情况下,foo 可能是一个恒等函数,然后就会创建一个替代路径,用已知的结果来替代它的调用。
我们来看一下操作码:
>>> def foo(n):
... f = lambda x:x
... return f(n)
...
>>> import dis
>>> dis.dis(foo)
2 0 LOAD_CONST 1 (<code object <lambda> at 0x7f177ade7608, file "<stdin>", line 2>)
3 MAKE_FUNCTION 0
6 STORE_FAST 1 (f)
3 9 LOAD_FAST 1 (f)
12 LOAD_FAST 0 (n)
15 CALL_FUNCTION 1
18 RETURN_VALUE
CPython(测试了2.7和3.3版本)似乎并没有优化掉这个lambda调用。也许其他实现会这样做?
>>> dis.dis(lambda x:x)
1 0 LOAD_FAST 0 (x)
3 RETURN_VALUE
恒等函数其实没做什么。所以每次你调用这个恒等函数时,基本上需要优化掉2个LOAD_FAST、1个CALL_FUNCTION和1个RETURN_VALUE,而创建一个可靠的替代路径(就像@viraptor说的,可能比看起来更复杂)则是另一回事。
也许在Python代码中使用else路径会更好。
你在最小/最大示例中真正的优化是通过存储结果来减少函数的调用次数。现在它只被调用n次,而不是n*4,这样的确是个不错的提升!