性能优化不一定非得动底层,很多时候只是换个表达方式,就能让内存占用和运行速度产生数量级的改善。这里介绍两个最常用、也最容易被忽略的代码级优化方向。
生成器替代列表:用多少算多少,而不是一次性全算出来
问题:当你写出 [x * 2 for x in range(10_000_000)] 时,Python 会立刻在内存中创建一个包含一千万个元素的列表。如果这个列表只是被循环使用一次,那这些内存就是纯粹的浪费,而且创建过程本身也会消耗大量时间。
解决方案:用生成器表达式或生成器函数替代列表。生成器不会一次性产生所有结果,而是在每次迭代时“现场”计算下一个值,用完就扔,内存占用几乎为零。
写法对比:
# ❌ 一次性生成整个列表——内存开销大,创建慢
squares_list = [x * x for x in range(10_000_000)]
for square in squares_list:
print(square) # 列表已完整存在于内存
# ✅ 生成器表达式——边用边算,内存友好
squares_gen = (x * x for x in range(10_000_000)) # 注意是圆括号
for square in squares_gen:
print(square) # 每次迭代计算一个值,旧值被丢弃
生成器函数的场景:
当逻辑复杂到无法用一行表达式写完,或者需要维护状态时,可以定义生成器函数。
def read_large_file(filename):
"""逐行读取大文件,避免整个文件一次性读入内存"""
with open(filename, 'r') as f:
for line in f:
yield line.strip()
for line in read_large_file('huge.log'):
process(line) # 每次只处理一行
适用判断:
- 你只需要遍历一次数据,或者不需要索引、不需要
len()、不需要多次访问——果断使用生成器。 - 你需要反复访问、切片、随机索引——则用列表。
- 很多标准库函数返回的已经是迭代器(如
map()、filter()、dict.keys()),无需再包一层list()除非必要。
一个容易犯错的细节:生成器只能遍历一次,遍历完就“空了”。如果需要多次使用,要么重新生成,要么提前转成列表。
合理使用数据结构:选对工具,效率翻倍
选择数据结构时,除了直觉,更要基于访问模式:
- 频繁的成员检查(in 操作):
list 的 in 是 O(n)(逐个扫描),set 和 dict 的 in 是 O(1)(基于哈希)。
当你在一个包含大量元素的列表中频繁查询元素是否存在时,换成 set 通常会获得几十到上千倍的性能提升。
# ❌ 如果 data 有 10 万条,每次检查都遍历整个列表
valid_ids = [1,2,3, ...] # 列表
if user_id in valid_ids: # 慢
...
# ✅ 转换为集合后检查
valid_ids = {1,2,3, ...} # 集合
if user_id in valid_ids: # 快
...
- 频繁在头部插入或弹出元素:
list.pop(0) 或 list.insert(0, item) 是 O(n),因为需要移动所有后续元素。
改用 collections.deque 双向队列,头尾操作都是 O(1)。
from collections import deque
q = deque()
q.appendleft(1) # 头部插入,O(1)
q.popleft() # 头部弹出,O(1)
- 需要统计计数:
手写 if key in dict: d[key] += 1 else: d[key] = 1 不仅丑陋而且慢。
直接用 collections.Counter,既清晰又是经过优化的实现。
from collections import Counter
word_counts = Counter(words) # 一行搞定词频统计
- 需要保留插入顺序的键值对:
从 Python 3.7 起,dict 已经保证插入顺序。如果需要额外的顺序敏感操作(如移动元素到末尾),可使用 collections.OrderedDict(尽管其使用场景已大大减少)。
- 选择不可变对象作为字典键:
用元组(tuple)而不是列表(list)作为字典的键,因为后者不可哈希。
- 数据量不大时,优先选择内置类型:
别过分追求理论上的最优复杂度。如果一个操作只需要处理几十条数据,用 list 完全没有问题,代码也更简单。复杂度优化在数据量达到数千以上时才会表现出显著差异。
小技巧:用 timeit 或 %%timeit(Jupyter)快速验证
对于不确定的选型,写两个版本的代码用 timeit 测试一下,用数据说话。一个典型的例子是:很多人认为字符串拼接用 += 会创建大量临时对象,但实际上在 Python 3 中针对字符串的 += 操作已经有优化,实践中用 join 还是 += 最好在具体场景下实测。
归根结底,性能优化来自于“理解底层机制”和“知道有哪些工具”的结合。生成器让你不必为“可能用不到”的数据提前买单,合理的数据结构让你的程序天生就运行在更短的时间复杂度上。两者都是“低成本、高回报”的优化手段。