为什么 any(True for ... if cond) 比 any(cond for ...) 快得多?

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()需要通过的元素较少。

  • 其他答案有更多细节,但我认为这个答案的简洁性和清晰度使其成为首先阅读的最佳答案。 (9认同)
  • 需要注意的是,由于它是一个生成器,“一切”并不一定意味着它处理了 `a` 的所有元素。当它们到达第一个奇数时,两个版本都停止。但是在测试用例中,没有奇数,因此生成器确实必须遍历整个列表。 (7认同)

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在第一种情况下,额外的来回产生并从生成器中获取下一个值占了开销。

  • @don't 这对我来说看起来像是微优化,所以你应该只在它真的对你有影响的情况下才去做,因为它在实际实践中可以明显地加速你的算法。我不会默认这种风格只是因为…… (7认同)
  • 那么第二种方式总是更快或同样快?那么总体来说是更可取的吗?到目前为止我一直使用第一种方式。 (2认同)

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),迭代器的家伙,无论何时被任何人询问下一个值,都不会盲目地只交出模值。相反,这个迭代器的家伙自己判断值,只有在有值得交出的东西时才会交出一些东西给经理。即只有当他发现一个奇数时(在这种情况下,他不交出01,而是True)。在这里,迭代器人就像一个能干的高级开发人员做所有的工作,而经理可以完全放松和放松(在一天结束的时候仍然要承担所有的功劳)。

应该清楚,第二种方式效率更高,因为它们不会为每个……单个……输入数字进行不必要的通信。特别是因为您的输入a = [0] * 10000000不包含任何奇数。初级开发人员向经理报告一千万个零,经理必须对所有零进行判断。对于每个零,它们之间不断来回。高级开发人员自行判断,向经理报告任何内容。好吧,好吧,最后两个开发人员还报告说他们已经完成了,此时经理得出的结论False是整个any(...)表达式的结果)。

  • 我已经阅读了所有三个答案。这是最令人困惑的一个,我仍然不知道是什么导致了所讨论的两个表达式之间的性能差异。“如果 x % 2 中的 x 为真”,难道不正是“any(x % 2 for x in a)”的作用吗? (3认同)
  • 包含在 `any(...)` 中,意思是我说的。我已经告诉你我*真正的意思*,你拒绝理解。因此投反对票。祝你今天过得愉快。 (3认同)
  • @oguzismail `True for x in a if x % 2` 甚至不是一个有效的表达式,你能澄清一下你的意思吗? (2认同)
  • @oguzismail 不,“True for x in a if x % 2”在Python中字面意思是“什么都没有”。这是[无效语法](https://tio.run/##K6gsycjPM7YoKPr/P6SoNFUhLb9IoUIhM08hUSEzDchSVTD6/x8A)。你能解决这个问题并说出你的真正意思吗? (2认同)
  • @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 的最小值”),但以不同的方式计算,速度也不同。 (2认同)
  • @oguzismail 我在答案中描述的不同方式。您的*“首先获取 x % 2 的结果并测试它是否为真”*不清楚,因为您没有说哪些对象执行这些操作。我看你是个很喜欢搞事情的人,所以让我试试吧。比较`cat number.txt | 打印_取模_2 | are_any_true` 和 `cat Numbers.txt | 打印_true_only_if_odd | are_any_true`。第二种方法更快,因为偶数行会被提前丢弃,不会通过管道发送到“are_any_true”。在 Python 代码中也是如此,生成器扮演“print_...”角色。。现在清楚了吗? (2认同)
  • 是的。你说的和投票最多的答案一样,用了更多的词。谢谢 (2认同)

Sor*_*ary 8

“检查虚假性”的数量并不是真正的原因,因为在更快的版本中我们可以看到ifintern 调用的语句bool()。在更快的情况下,该检查是“提前”完成的。因此,在这两种情况下,python 都必须遍历所有值并检查所有值的真实性。

Chepner的回答中显示的过程确实是问题的答案。让我们看看何时可以请求 for 循环中的下一个项目...:

在更快的情况下,它就在 后面BINARY_MODULOPOP_JUMP_IF_FALSE声明中它必须做一些工作来检查真实性if调用bool()),而在较慢的版本中它不会检查那里。到目前为止(-1)点以获得更快的版本。但在较慢的版本中,它必须执行三个步骤才能到达请求下一个项目的点YIELD_VALUE,,,,POP_TOPJUMP_ABSOLUTE所以(-3)对于较慢的版本...这三个步骤会导致开销,因为它们不能被跳过。

换句话说,较快的版本仅执行“检查”以到达请求下一个项目的点,但较慢的版本必须执行“检查+这些步骤”。他们都再次检查所有值的真实性。

答案是屈服的开销。

  • @donttalkjustcode 因为这会偏离主题,所以对于最后一个问题,我说 `if obj:` 语句的作用正是 `if bool(obj) is True:` 的作用,但你拒绝了它,并且说了别的东西调用`__bool__`。你能告诉我这是如何工作的吗?`if obj:` 还检查是否从 `obj` 的 `__bool__` 方法返回布尔值。 (2认同)
  • 我认为它必须是“tp_as_number-&gt;nb_bool”选项,也许“as_number”只是一个误导性的名称。 (2认同)