为什么 any(True for … if cond) 比 any(cond for …) 快得多?
检查列表是否包含奇数的两种类似方法:
any(x % 2 for x in a)
any(True for x in a if x % 2)
计时结果a = [0] * 10000000(每次尝试五次,时间以秒为单位):
any(x % 2 for x in a)
any(True for x in a if x % 2)
为什么第二种方式几乎快两倍?
我的测试代码:
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)
在线试试吧!
回答
第一种方法将所有内容发送到,any()而第二种方法仅any()在有奇数时发送,因此any()需要通过的元素较少。
- 其他答案有更多细节,但我认为这个答案的简洁性和清晰度使其成为首先阅读的最佳答案。
- 需要注意的是,由于它是一个生成器,“一切”并不一定意味着它处理了 `a` 的所有元素。当它们到达第一个奇数时,两个版本都停止。但是在测试用例中,没有奇数,因此生成器确实必须遍历整个列表。
(x % 2 for x in a)
这个生成器会产生一系列的假值,直到它产生一个真值(如果确实如此),此时any将停止迭代生成器并返回True。
(True for x in a if x % 2)
这个生成器只会产生一个True值(如果有的话),此时any将停止迭代并返回True。
any在第一种情况下,额外的来回产生并从生成器中获取下一个值占了开销。
- @don't 这对我来说看起来像是微优化,所以你应该只在它真的对你有影响的情况下才去做,因为它在实际实践中可以明显地加速你的算法。我不会默认这种风格只是因为……
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
两者在BINARY_MODULO指令上都是相同的。之后,较慢的版本必须any在继续之前产生结果值以供消费,而第二个代码立即移动到下一个值。所以基本上,较慢的代码必须消耗一长串假(即非零)值来确定没有真值。更快的代码只需要消耗一个空列表。
前面的答案在某种程度上假设读者已经熟悉语法和生成器。我想为那些不是的人解释更多。
片段
any(x % 2 for x in a)
是以下的简短语法:
any((x % 2 for x in a))
所以发生的事情是(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(...)表达式的结果)。
- 我已经阅读了所有三个答案。这是最令人困惑的一个,我仍然不知道是什么导致了所讨论的两个表达式之间的性能差异。“如果 x % 2 中的 x 为真”,难道不正是“any(x % 2 for x in a)”的作用吗?
- 包含在 `any(...)` 中,意思是我说的。我已经告诉你我*真正的意思*,你拒绝理解。因此投反对票。祝你今天过得愉快。
- @oguzismail 我不想知道您的意思是代码的“含义”,我想知道您的意思是 *code*。因为我无法讨论我不知道的代码。所以你的意思是`any(True for x in a if x % 2)`吗?是的,`any(x % 2 for x in a)`和`any(True for x in a if x % 2)`具有相同的含义,几乎“如果 x % 2 对任何你说的“a”中的元素。但是他们以*不同的方式*实现了这一目标,其中之一更快。就像`min(a)` 和`sorted(a)[0]` 意思相同(“a 的最小值”),但以不同的方式计算,速度也不同。
- 是的。你说的和投票最多的答案一样,用了更多的词。谢谢