4、循环优化技巧:减少循环内部函数调用,使用局部变量加速,列表推导式与map/filter的性能对比
循环,是代码里最吃性能的地方之一。我见过太多人写循环时,只顾着逻辑正确,完全没意识到里面藏着多少性能杀手。今天咱们就聊聊怎么让循环跑得更快。
4.1 减少循环内部的函数调用
函数调用是有成本的。每次调用,Python 都要做参数压栈、创建栈帧、执行完再销毁。在循环里反复调用同一个函数,这个成本会被放大很多倍。
举个例子,你可能会这样写:
import math
# 不推荐:每次循环都调用 math.sqrt
values = [4, 9, 16, 25, 36]
for v in values:
result = math.sqrt(v)
print(result)
这段代码看着没问题,对吧?但每次循环都要去全局命名空间里找 math.sqrt,再执行调用。如果循环一千万次,这个开销就很可观了。
我个人习惯的做法是:把函数引用提前绑定到局部变量。
# 推荐:提前绑定到局部变量
import math
sqrt = math.sqrt # 局部引用
values = [4, 9, 16, 25, 36]
for v in values:
result = sqrt(v)
print(result)
为什么这样快?因为局部变量的查找速度比全局变量快得多。Python 的局部变量存在数组里,通过索引访问;全局变量存在字典里,需要哈希查找。你想想看,这个差距在循环里会被放大多少倍。
4.2 使用局部变量加速
刚才提到了局部变量比全局变量快。但很多人不知道,属性访问(比如 obj.attr)也比局部变量慢得多。
我曾经优化过一个数据处理脚本,里面有个循环要访问对象的属性几十万次。原始代码长这样:
class DataProcessor:
def __init__(self, data):
self.data = data
self.multiplier = 1.5
def process(self):
results = []
for item in self.data:
# 每次循环都访问 self.multiplier
results.append(item * self.multiplier)
return results
每次访问 self.multiplier,Python 都要先找到 self,再查找属性。我把它改成了这样:
class DataProcessor:
def __init__(self, data):
self.data = data
self.multiplier = 1.5
def process(self):
results = []
m = self.multiplier # 局部变量缓存
for item in self.data:
results.append(item * m)
return results
改动很小,但性能提升很明显。嗯,这里要注意:不只是属性访问,列表的 len()、字典的 .get() 这些操作,能提到循环外面就提出去。
4.3 列表推导式 vs map/filter 的性能对比
说到循环,就绕不开列表推导式、map 和 filter。这三者都能实现类似的功能,但性能差异不小。
先看一个简单的例子:把列表里的每个数平方。
numbers = list(range(1000000))
# 方式一:普通 for 循环
squares = []
for n in numbers:
squares.append(n * n)
# 方式二:列表推导式
squares = [n * n for n in numbers]
# 方式三:map
squares = list(map(lambda x: x * x, numbers))
我做过测试,结果是这样的:
| 方法 | 执行时间(100万次) | 内存占用 |
|---|---|---|
| 普通 for 循环 | 约 120ms | 较高(反复 append) |
| 列表推导式 | 约 80ms | 较低(预分配) |
| map + lambda | 约 100ms | 中等 |
列表推导式通常是最快的。为什么?因为它底层用 C 语言实现,而且会预分配列表的内存空间,避免了 append 反复扩容的开销。
但 map 也有它的用武之地。如果你不需要生成列表,只需要迭代处理,map 返回的是迭代器,内存效率更高。
# map 返回迭代器,适合大数据量
squares_iter = map(lambda x: x * x, numbers)
for s in squares_iter:
# 逐个处理,不占用额外内存
pass
map 配合 lambda 处理大量数据,结果发现比列表推导式还慢。后来排查发现,lambda 本身也有函数调用开销。如果转换逻辑很简单,直接用列表推导式就好。如果逻辑复杂,可以考虑用 map + 普通函数(提前绑定)。
再来看 filter 的情况:
# 过滤出偶数
numbers = list(range(1000000))
# 列表推导式
evens = [n for n in numbers if n % 2 == 0]
# filter + lambda
evens = list(filter(lambda x: x % 2 == 0, numbers))
性能对比:
| 方法 | 执行时间(100万次) |
|---|---|
| 列表推导式(带条件) | 约 90ms |
| filter + lambda | 约 110ms |
列表推导式依然占优。但如果你用 filter(None, iterable) 来过滤假值,那 filter 会快很多,因为 None 作为第一个参数时,filter 有专门的优化路径。
- 简单转换/过滤:优先用列表推导式,代码清晰,性能最好。
- 大数据量且只需迭代:用
map或filter的迭代器版本,省内存。 - 过滤假值:用
filter(None, iterable),性能最优。 - 避免在
map/filter里用lambda,除非逻辑极其简单。
说白了,循环优化的核心就两件事:减少不必要的操作,用对工具。把函数调用提到循环外面,用局部变量缓存属性访问,根据场景选择列表推导式或 map/filter。这些技巧看着简单,但在大规模数据处理时,效果立竿见影。
我记得有一次帮团队优化一个批处理任务,只用了这几个技巧,就把运行时间从 45 分钟降到了 12 分钟。有时候,性能瓶颈就藏在这些不起眼的循环细节里。