
写Python脚本的时候,循环是最常用的东西。数据一多,循环就成了瓶颈。我见过一个统计日志的脚本,跑一次要四十多分钟。改了三处地方,两分半跑完。机器没换,代码逻辑没变,只是换了几种写法。下面说的三招,都是我自己在项目里反复用过的。没有理论推导,只讲怎么改、为什么快。
第一招:把循环里能提前算好的东西拿出来很多人写循环喜欢把所有计算都塞进循环体。比如每次迭代都调用一次len(),或者反复算同一个乘法。Python的函数调用开销比你想的大。循环一万次,里面调用一万次len(),就是一万次函数查找和调用。把len()的结果存到一个变量里,循环体里直接用变量。看个例子。假设要处理一个列表,对每个元素做除法,分母是列表长度。
data = [1, 2, 3, ...] # 假设十万个元素 for i in range(len(data)): result = data[i] / len(data)
每次循环都算一次len(data)。改成这样:
n = len(data) for i in range(n): result = data[i] / n
速度快多少?我实测过十万次循环,大概快15%到20%。别小看这点。循环体里还有别的操作,累积起来就明显了。更隐蔽的情况是属性查找。比如循环里反复用obj.attr。Python每次都要查字典。提前把attr存到局部变量:
attr = obj.attr for item in items: use(attr, item)
这一招对模块级函数也管用。比如循环里调math.sqrt,可以写成sqrt = math.sqrt。局部变量查找比全局快,比属性查找更快。第二招:用内置函数和列表推导替代手写循环Python的内置函数是用C写的。你写一个for循环做累加,每次迭代都要走Python虚拟机的字节码。sum()函数直接在C层面遍历,没有Python层的开销。比如要对一个数字列表求和:
total = 0 for x in nums: total += x
换成sum(nums)。数据量越大,差距越明显。一百万个数,手写循环大概要0.05秒,sum()只要0.005秒。十倍。列表推导也是同样的道理。下面这种写法很常见:
squares = [] for x in range(100000): squares.append(x * x)
改成列表推导:
squares = [x * x for x in range(100000)]
列表推导在字节码层面做了优化,没有反复调用append方法。速度提升大概30%到40%。如果逻辑更复杂,可以用生成器表达式配合内置函数。比如要算平方和:
total = sum(x * x for x in range(100000))
这比先建列表再求和更省内存,速度也不慢。生成器没有一次性分配整个列表的开销。还有一个常用的:map和filter。当你要对每个元素做同一个函数调用时,map比循环快。比如把所有字符串转成整数:
strs = ['1', '2', '3', ...] ints = list(map(int, strs))
这比写for循环逐个转换要快。因为map的迭代和函数调用都在C层面完成。filter类似。第三招:减少循环体内的重复计算和对象创建循环里创建对象是很贵的。比如每次迭代都建一个临时列表、一个字典、一个字符串。这些对象创建后很快被丢弃,垃圾回收器要不断工作。看个例子。要拼接字符串:
result = '' for s in strings: result += s + ','
每次+=都创建一个新字符串。字符串是不可变的。一百万个字符串拼接,内存里会产生一百万个中间字符串。速度慢得惊人。改成用列表收集,最后join:
parts = [] for s in strings: parts.append(s) result = ','.join(parts)
append只是往列表里加引用,不创建新字符串。join一次性分配最终字符串的内存。速度快几十倍。如果逻辑简单,直接写:
result = ','.join(strings)
更直接。join的参数可以是任何可迭代对象,不需要先转列表。循环里反复创建字典或集合也是同样的问题。如果这些容器在每次迭代中结构相同,只是值不同,考虑能不能复用。比如:
for x in data: temp = {'a': x, 'b': x * 2} process(temp)
如果process不修改temp,可以改成:
temp = {'a': 0, 'b': 0} for x in data: temp['a'] = x temp['b'] = x * 2 process(temp)
这样只创建了一个字典。但要注意:如果process把temp存起来了,后面迭代会覆盖。这种情况不能复用。判断标准很简单:process有没有保留引用。没有就安全。还有循环里的条件判断。如果条件在循环中不变,提到外面。比如:
for x in data: if mode == 'fast': result = x * 2 else: result = x + 1
mode在循环里不会变。可以拆成两个循环:
if mode == 'fast': for x in data: result = x * 2 else: for x in data: result = x + 1
这样每个循环里少了一次比较。数据量大时,省下的时间可观。代码稍微长一点,但值得。再提一个细节:range和enumerate的选择。如果你不需要索引,直接for x in data。不要写for i in range(len(data)): x = data[i]。后者多了一次索引查找。如果同时需要索引和值,用enumerate。不要手动维护计数器。这三招不是孤立的。实际优化时经常一起用。我改过一个文本处理脚本,原来用for循环逐行读文件,每行做split,然后循环处理每个字段,里面还反复调len()和创建临时列表。改法:用with open直接迭代文件对象,split结果存变量,len提前算,临时列表改成生成器。四十多分钟变成两分半。十倍不止。最后说一个容易被忽略的点:嵌套循环。两层循环,内层循环里做和外层变量无关的计算。比如:
for i in range(1000): for j in range(1000): result = i * j + offset
offset不变。提到外层循环外面。如果内层循环里用了外层的某个表达式,而这个表达式只依赖外层变量,可以提到内层循环外面。比如:
for i in range(1000): base = i * 2 for j in range(1000): result = base + j
这样内层循环少了一次乘法。嵌套循环的优化收益是乘积级的。外层一千次,内层一千次,省一次乘法就是省一百万次。你平时写循环有没有遇到跑得特别慢的情况?把那段代码贴出来,我帮你看看能不能提速。
以上就是“Python代码优化:别让循环拖后腿,三招提速十倍!”的详细内容,想要了解更多Python教程欢迎持续关注编程学习网。
扫码二维码 获取免费视频学习资料

- 本文固定链接: http://www.phpxs.com/post/14518/
- 转载请注明:转载必须在正文中标注并保留原文链接
- 扫码: 扫上方二维码获取免费视频资料