为什么enumerate这么慢?

编程语言 2026-07-11

这个问题以前也被问过 asked before,但我没有看到关于在现代Python中为何会是这样的合理解释。

我已经搭好了一个测试装置来试图证明这一点。

这里有两个函数。每个函数都达到完全相同的目标(它实际在做什么并不重要)。一个正确地使用了enumerate(并且可能被认为更符合 pythonic 的风格),另一个使用一个离散变量进行手动修改。否则功能完全相同。

对要执行的函数进行伪随机选择可能显得有点“过头”,但我是在尝试消除任何潜在的Python内部缓存(如果确实存在这样的东西的话)。

使用enumerate的那个函数的运行速度始终比另一个慢大约7.5%。

下面是代码:

from math import comb
from timeit import timeit
from random import choice
from collections import defaultdict


def pdiff(v1: float, v2: float) -> float:
    return (v1 - v2) / ((v1 + v2) / 2) * 100.0


def func1(s: str) -> bool:
    digits = [int(d) for d in s]
    n2 = len(s) - 2
    a = b = i = 0

    for d1, d2 in zip(digits, digits[1:]):
        c = comb(n2, i)
        a += c * d1
        b += c * d2
        i += 1

    return a % 10 == b % 10


def func2(s: str) -> bool:
    digits = [int(d) for d in s]
    n2 = len(s) - 2
    a = b = 0

    for i, (d1, d2) in enumerate(zip(digits, digits[1:]), 0):
        c = comb(n2, i)
        a += c * d1
        b += c * d2

    return a % 10 == b % 10


if __name__ == "__main__":
    data = "1234567890"

    assert func1(data) == func2(data)

    functions = (func1, func2)

    analysis = defaultdict(list)

    for _ in range(50):
        function = choice(functions)
        duration = timeit(lambda: function(data), number=250_000)
        name = function.__name__
        analysis[name].append(duration)

    durations = []

    for k, v in analysis.items():
        average = sum(v) / len(v)
        print(f"{k} {average:.4f}s")
        durations.append(average)

    assert len(durations) == 2

    print(f"{pdiff(*durations):.2f}%")

平台:

  • Apple Silicon(M2)
  • MacOS 26.3.1
  • Python 3.14.3

解决方案

你看到的并不是 enumerate 出了什么“错误”,只是这两个循环结构安排的副作用。

在你的第二个版本中,每次迭代都要经过额外的一层:zip 产生一个对,然后 enumerate 将其包裹成带索引的另一个元组,接着Python必须解包这个嵌套的结构。这比简单地让局部变量自增多了一些工作。

在第一版中,i 只是一个普通的局部变量,而 i += 1 已经尽可能地便宜了。因此在紧密循环中,结果略微快一些也就不足为奇。

现在,关于基准测试本身——对于这么小的量级,它并不理想:

  • 你是在对一个非常短的输入进行循环,因此微小的差异会被放大。
  • 循环体执行的是一个相对昂贵的操作(comb),这让你更难分离出你真正想要衡量的部分。
  • 随机选择要运行的函数并没有真正的帮助,反而增加了噪声。
  • 把调用包装在lambda里面也会带来额外的开销。

所以,是的,在这里看到大约7% 的差异是合理的,但这并不意味着 enumerate 在一般情况下“慢”。它只是意味着在这个特定的微观场景中,手动计数器更便宜一点。

如果你关心这里的性能,最大的收益不是替换 enumerate,而是在每次迭代中避免重新计算组合。这才是真正的成本所在。

至于你的观点:我之所以提CPython,是因为它是大多数人所使用的参考解释器,但原理并不依赖于它——关键是每次迭代额外做的工作量。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章