人人都会AI编程

生成器替代列表、合理使用数据结构

更新时间:2026-07-12

性能优化不一定非得动底层,很多时候只是换个表达方式,就能让内存占用和运行速度产生数量级的改善。这里介绍两个最常用、也最容易被忽略的代码级优化方向。

生成器替代列表:用多少算多少,而不是一次性全算出来

问题:当你写出 [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 操作)

listin 是 O(n)(逐个扫描),setdictin 是 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 还是 += 最好在具体场景下实测。

归根结底,性能优化来自于“理解底层机制”和“知道有哪些工具”的结合。生成器让你不必为“可能用不到”的数据提前买单,合理的数据结构让你的程序天生就运行在更短的时间复杂度上。两者都是“低成本、高回报”的优化手段。