Python技术迷

Python 实现循环的最快方式(for、while 等速度对比)

先把丢结论:在 CPython 里,做“很多次、每次很简单”的循环,纯 Python 代码的解释器开销通常比“你要做的事”更贵。所以:

  • 计数型循环:for i in range(n) 通常比 while 更快更稳。
  • 对序列遍历:for x in seq 通常就是最优解;需要索引时优先 for i, x in enumerate(seq),少写 range(len(seq))。
  • 做聚合(累加、拼接)时:尽量把工作交给内置函数或 C 实现(比如 sum(...)、b''.join(...)、array/deque、numpy),少在 Python 层一项项 append。
  • 大循环里反复用的方法/属性,先绑定到局部变量(把属性查找挪出循环),常见的就是 append = lst.append。
  • 能向量化就向量化:numpy > 纯 Python 循环,数量级差异。
  • 解释器不同也有差别:PyPy 对纯 Python 循环常常明显快于 CPython。

下面给一套可直接跑的基准脚本,涵盖 for、while、enumerate、列表推导、生成器、map、以及简单的 numpy 对比,并顺手展示“局部变量绑定”到底能快多少。

说明:不同机器、Python 版本、编译选项都会影响数值,请以你本地结果为准;代码里把工作量调在 10^7 级别会比较慢,可先从 10^6 试起。

import time
import timeit
from array import array

try:
import numpy as np
except Exception:
    np = None

N = 1_000_000  # 先从 100万 试跑,觉得快再上 1千万

defloop_for_sum():
    s = 0
for i in range(N):
        s += i
return s

defloop_while_sum():
    s = 0
    i = 0
while i < N:
        s += i
        i += 1
return s

defloop_for_enumerate_sum():
# enumerate 主要在需要索引+值时更香,这里只是演示其开销
    s = 0
for i, _ in enumerate(range(N)):
        s += i
return s

defloop_for_append_attr_lookup():
# 每次都做 lst.append 的属性查找
    lst = []
for i in range(N):
        lst.append(i)
return lst

defloop_for_append_local_binding():
# 先把 append 绑定到局部变量,避免每次属性查找
    lst = []
    append = lst.append
for i in range(N):
        append(i)
return lst

defcomp_list():
# 列表推导式在 C 层循环,通常比 for-append 快
return [i for i in range(N)]

defcomp_sum_builtin():
# 内置 sum 也是 C 实现的循环,常比手写 for 更快
return sum(range(N))

defgen_sum():
# 生成器配合 sum,依然只有一次 C 层 sum 循环
return sum(i for i in range(N))

defmap_sum():
# map 在 C 层调度迭代,但传入的是 Python 函数,未必更快
return sum(map(int, range(N)))   # 只是演示用法

defarray_append_local_binding():
# array('l') 连续内存,append 绑定也有用
    a = array('l')
    app = a.append
for i in range(N):
        app(i)
return a

defnumpy_sum():
if np isNone:
returnNone
    arr = np.arange(N, dtype=np.int64)  # 向量化生成
return int(arr.sum())

defbench_once(fn):
    t0 = time.perf_counter()
    r = fn()
    t1 = time.perf_counter()
return r, t1 - t0

defpretty(ms):
returnf"{ms*1000:.1f} ms"

defmain():
# 为了可读,部分测试单跑一次;速度对比看耗时即可
    tests = [
        ("for 累加", loop_for_sum),
        ("while 累加", loop_while_sum),
        ("enumerate 累加", loop_for_enumerate_sum),
        ("for+append(属性查找)", loop_for_append_attr_lookup),
        ("for+append(局部绑定)", loop_for_append_local_binding),
        ("列表推导 构建列表", comp_list),
        ("sum(range(N))", comp_sum_builtin),
        ("sum(生成器)", gen_sum),
        ("sum(map(int, range))", map_sum),
        ("array.append(局部绑定)", array_append_local_binding),
    ]
if np isnotNone:
        tests.append(("NumPy 向量化求和", numpy_sum))

    print(f"Python: {__import__('sys').version.split()[0]}, N={N}")
for name, fn in tests:
# 对快函数多跑几次更稳定
        repeat = 3if"sum(range"in name or"for 累加"in name or"while"in name else1
        best = float("inf")
        ret_keep = None
for _ in range(repeat):
            _, dt = bench_once(fn)
if dt < best:
                best = dt
        print(f"{name:>22s}  ->  {pretty(best)}")

if __name__ == "__main__":
    main()

为什么会这样:解释器开销 vs. C 层循环

  • for i in range(n) 为什么常胜?range 对象在 C 层迭代,无需每次创建新整数(Py3 中的 range 是惰性的不可变序列),而 while 需要你在 Python 层做条件判断、加法、比较,解释器每步都在跑字节码;两者都跑字节码,但 for 在取下一个值时的分支和边界处理更“内联”,常见基准下略优。

  • 列表推导为什么比 for+append 快?推导式的“循环框架”在 C 层展开,一次性把 append 的调用路径固定下来;而 for+append 每次都要做一次 lst.append 的 属性查找(从对象字典里找方法,再创建绑定方法对象),很花时间。

  • 把 append 绑定到局部变量能快多少?Python 局部变量访问是 LOAD_FAST,对象属性访问是 LOAD_ATTR 还要查找、生成绑定方法对象;把 append = lst.append 挪到循环外,只做一次属性查找。对百万元素的循环,这个小优化往往就是 10%~30% 的差距。

  • sum(range(n)) 为什么更快?sum 的循环在 C 层,range 的取值在 C 层,两层的“辛苦活”都不是 Python 字节码在干,自然更快。 但注意:你实际做的工作(比如里头要做复杂逻辑)如果不能放进 sum 这样的小而美的接口,就还是要回到 Python 循环。

  • map/生成器map(func, seq) 会频繁回到 Python 层调用 func,这就把解释器开销又拉回来了;而 sum(i for i in seq) 的“生成器部分”是 Python 层,sum 只负责拉取并相加,通常与 sum(range(n)) 接近但略慢。

  • 向量化一旦可以用 numpy 把计算转成 一次大批量 的 C/Fortran 计算,成本就被摊薄到极致,通常能比纯 Python 循环快 一个数量级以上。缺点是需要把问题转成数组运算模型。

常见场景怎么写更快

遍历与计数

  • 只需要次数:for _ in range(n): ...
  • 需要索引+值:for i, x in enumerate(seq): ...(比 range(len(seq)) 可读且差不多快)
  • 需要过滤再处理:尽量把过滤放到 推导式/生成器 里,或用 itertools.compress/filterfalse。

构建容器

  • 列表:能推导就推导:[f(x) for x in seq]。
  • 逐个 append 必须写时:append = lst.append 放到循环前。
  • 字典/集合:尽量用推导式:{k: f(k) for k in keys} / {f(x) for x in seq}。
  • 二进制拼接:用 b''.join(chunks),不要在循环里 b += piece。

聚合

  • 和、最值、计数:sum / max / min / collections.Counter。
  • 前缀和/累积:itertools.accumulate。
  • 任意/全部:any / all(短路,且在 C 层循环)。

I/O 与循环

  • 文件逐行处理已是 C 层迭代:for line in f: 很快;避免 readlines() 一次性拉进来。
  • 反复 print 很慢,尽量收集后一次性写入或用 write 缓冲。

小坑与版本差异

  • CPython 3.12+ 做了不少字节码和解释器微优化,for/推导式进一步受益;但总体结论不变:尽量让 C 层干活。
  • PyPy 的 JIT 对“热循环”很友好,纯 Python 循环可能大幅提速;如果你的工作负载是“长循环+简单操作”,值得试一下 PyPy。
  • 字符串在循环里 += 拼接是反例,会产生 O(n²) 拷贝;务必收集到列表再 ''.join。

一个更贴近实战的小例子

把日志里指定等级的行摘出来并解析时间戳,三种写法的思路差不多,但尽量用“C 层迭代 + 局部绑定 + 推导式/内置”组合拳。

from datetime import datetime

defparse_bad(lines):
    out = []
for line in lines:
if"[INFO]"in line:
            ts = line.split()[0]  # 假设第一列是时间
            out.append(datetime.fromisoformat(ts))
return out

defparse_better(lines):
# 绑定到局部,少全局查找
    out = []
    append = out.append
    fromiso = datetime.fromisoformat
for line in lines:
if"[INFO]"in line:
            append(fromiso(line.split()[0]))
return out

defparse_pythonic(lines):
    fromiso = datetime.fromisoformat
return [fromiso(line.split()[0]) for line in lines if"[INFO]"in line]

大多数情况下,parse_pythonic 会是最短也最快的;当内部逻辑更复杂、推导式不易读时,用 parse_better 的“局部绑定”手法。

该怎么测:稳定的基准姿势

  • 用 timeit 或上面的 bench_once 包一层;
  • 预热(先跑几次不计时);
  • 控制变量(相同输入、相同 Python 版本);
  • 多跑几轮取最好或中位数;
  • 把 N 调到足够大,避免噪声。

一句话记忆:让 C 来跑循环;不得不在 Python 跑,就让解释器少做事(少属性查找、少函数回调、用局部变量、用推导/内置),能向量化就向量化。

-END-

我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html

🔥虎哥私藏精品🔥

虎哥作为一名老码农,整理了全网最全《python高级架构师资料合集》,总量高达650GB,点击下方公众号回复关键字 python 全部免费领