为什么 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),迭代器的家伙,无论何时被任何人询问下一个值,都不会盲目地只交出模值。相反,这个迭代器的家伙自己判断值,只有在有值得交出的东西时才会交出一些东西给经理。即只有当他发现一个奇数时(在这种情况下,他不交出01,而是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 的最小值”),但以不同的方式计算,速度也不同。
  • 是的。你说的和投票最多的答案一样,用了更多的词。谢谢

以上是为什么 any(True for … if cond) 比 any(cond for …) 快得多?的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>