don*_*ode 69 python performance
检查列表是否包含奇数的两种类似方法:
any(x % 2 for x in a)
any(True for x in a if x % 2)
Run Code Online (Sandbox Code Playgroud)
计时结果a = [0] * 10000000
(每次尝试五次,时间以秒为单位):
any(x % 2 for x in a)
any(True for x in a if x % 2)
Run Code Online (Sandbox Code Playgroud)
为什么第二种方式几乎快两倍?
我的测试代码:
from timeit import repeat
setup = 'a = [0] * 10000000'
expressions = [
'any(x % 2 for x in a)',
'any(True for x in a if x % 2)',
]
for expression in expressions:
times = sorted(repeat(expression, setup, number=1))
print(*('%.2f ' % t for t in times), expression)
Run Code Online (Sandbox Code Playgroud)
Adr*_*ert 73
第一种方法将所有内容发送到,any()
而第二种方法仅any()
在有奇数时发送,因此any()
需要通过的元素较少。
dec*_*eze 69
(x % 2 for x in a)
Run Code Online (Sandbox Code Playgroud)
这个生成器会产生一系列的假值,直到它产生一个真值(如果确实如此),此时any
将停止迭代生成器并返回True
。
(True for x in a if x % 2)
Run Code Online (Sandbox Code Playgroud)
这个生成器只会产生一个True
值(如果有的话),此时any
将停止迭代并返回True
。
any
在第一种情况下,额外的来回产生并从生成器中获取下一个值占了开销。
che*_*ner 27
TL;DR 慢版本必须在返回之前迭代一长串假值False
。快速版本在执行相同操作之前“迭代”一个空序列。不同之处在于构建长假序列与空序列所需的时间。
让我们看看每个生成的字节码。我省略了每个的第一部分,因为它们对于两者都是相同的。我们只需要查看所涉及的生成器的代码。
In [5]: dis.dis('any(x%2 for x in a)')
[...]
Disassembly of <code object <genexpr> at 0x105e860e0, file "<dis>", line 1>:
1 0 LOAD_FAST 0 (.0)
>> 2 FOR_ITER 14 (to 18)
4 STORE_FAST 1 (x)
6 LOAD_FAST 1 (x)
8 LOAD_CONST 0 (2)
10 BINARY_MODULO
12 YIELD_VALUE
14 POP_TOP
16 JUMP_ABSOLUTE 2
>> 18 LOAD_CONST 1 (None)
20 RETURN_VALUE
In [6]: dis.dis('any(True for x in a if x % 2)')
[...]
Disassembly of <code object <genexpr> at 0x105d993a0, file "<dis>", line 1>:
1 0 LOAD_FAST 0 (.0)
>> 2 FOR_ITER 18 (to 22)
4 STORE_FAST 1 (x)
6 LOAD_FAST 1 (x)
8 LOAD_CONST 0 (2)
10 BINARY_MODULO
12 POP_JUMP_IF_FALSE 2
14 LOAD_CONST 1 (True)
16 YIELD_VALUE
18 POP_TOP
20 JUMP_ABSOLUTE 2
>> 22 LOAD_CONST 2 (None)
24 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)
两者在BINARY_MODULO
指令上都是相同的。之后,较慢的版本必须any
在继续之前产生结果值以供消费,而第二个代码立即移动到下一个值。所以基本上,较慢的代码必须消耗一长串假(即非零)值来确定没有真值。更快的代码只需要消耗一个空列表。
Kel*_*ndy 22
前面的答案在某种程度上假设读者已经熟悉语法和生成器。我想为那些不是的人解释更多。
片段
any(x % 2 for x in a)
Run Code Online (Sandbox Code Playgroud)
是以下的简短语法:
any((x % 2 for x in a))
Run Code Online (Sandbox Code Playgroud)
所以发生的事情是(x % 2 for x in a)
得到评估,然后将结果值提供给any
函数。就像print(21 * 2
) 计算值 42,然后将其提供给print
函数。
该表达式(x % 2 for x in a)
是一个生成器表达式,其结果是一个生成器迭代器。这是一个根据需要计算和分发其值的对象。因此,在这种情况下,当被要求提供一个值时,该迭代器查看 中的下一个值a
,计算其余数模 2(即,0 表示偶数,1 表示奇数),并分发该值。然后从字面上等待可能再次被要求提供另一个值。
该any
功能是第二男主角在这里。它获取迭代器作为其参数,然后向迭代器询问越来越多的值,希望有一个为真(注意 1 为真,0 为假)。
你真的可以把它想象成两个不同的人在互动。任何人向迭代器人询问值。再次,注意,迭代器的家伙并没有计算提前的所有值。一次只有一个,每当任何人要求下一个值时。所以这真的是两个人之间的来回。
如果是 any(x % 2 for x in a)
,迭代器人员,每当任何人询问下一个值时,只需计算一个模值,将其交给任何人,任何人必须对其进行判断。在这里,迭代器家伙就像一个不称职的初级开发人员,每个数字都涉及经理,有点迫使他们进行核心微观管理。
在 的情况下any(True for x in a if x % 2)
,迭代器的家伙,无论何时被任何人询问下一个值,都不会盲目地只交出模值。相反,这个迭代器的家伙自己判断值,只有在有值得交出的东西时才会交出一些东西给经理。即只有当他发现一个奇数时(在这种情况下,他不交出0
或1
,而是True
)。在这里,迭代器人就像一个能干的高级开发人员做所有的工作,而经理可以完全放松和放松(在一天结束的时候仍然要承担所有的功劳)。
应该清楚,第二种方式效率更高,因为它们不会为每个……单个……输入数字进行不必要的通信。特别是因为您的输入a = [0] * 10000000
不包含任何奇数。初级开发人员向经理报告一千万个零,经理必须对所有零进行判断。对于每个零,它们之间不断来回。高级开发人员自行判断,不向经理报告任何内容。好吧,好吧,最后两个开发人员还报告说他们已经完成了,此时经理得出的结论False
是整个any(...)
表达式的结果)。
“检查虚假性”的数量并不是真正的原因,因为在更快的版本中我们可以看到if
intern 调用的语句bool()
。在更快的情况下,该检查是“提前”完成的。因此,在这两种情况下,python 都必须遍历所有值并检查所有值的真实性。
Chepner的回答中显示的过程确实是问题的答案。让我们看看何时可以请求 for 循环中的下一个项目...:
在更快的情况下,它就在 后面BINARY_MODULO
,但在POP_JUMP_IF_FALSE
声明中它必须做一些工作来检查真实性(if
调用bool()
),而在较慢的版本中它不会检查那里。到目前为止(-1)点以获得更快的版本。但在较慢的版本中,它必须执行三个步骤才能到达请求下一个项目的点YIELD_VALUE
,,,,POP_TOP
。JUMP_ABSOLUTE
所以(-3)对于较慢的版本...这三个步骤会导致开销,因为它们不能被跳过。
换句话说,较快的版本仅执行“检查”以到达请求下一个项目的点,但较慢的版本必须执行“检查+这些步骤”。他们都再次检查所有值的真实性。
答案是屈服的开销。