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 全部免费领