处理大量数据时,先把所有结果装进列表再慢慢用,往往会白白吃掉大量内存。Python 的生成器(generator)提供了一种"用到才产出"的惰性思路:数据一个一个给,内存里永远只放当前这一个。本篇讲清楚生成器是什么、怎么写、什么时候用。

为什么需要生成器

先看一组对比。列表推导式会立刻把所有元素算出来放进内存:

nums = [x * x for x in range(5)]
print(nums)        # 输出:[0, 1, 4, 9, 16]

gen = (x * x for x in range(5))  # 只是把 [] 换成 ()
print(gen)         # 输出:<generator object <genexpr> at 0x...>
print(next(gen))   # 输出:0
print(next(gen))   # 输出:1

圆括号版本叫生成器表达式,它返回的不是列表,而是一个生成器对象。调用 next() 时它才算出下一个值——数据是"按需生产"的。如果序列有一千万个元素,列表要把一千万个结果全存下来,生成器却始终只保存"当前算到哪了"。

yield:把普通函数变成生成器

函数体里只要出现 yield 关键字,这个函数就不再是普通函数,而是"生成器工厂":调用它不会执行函数体,而是返回一个生成器对象。

def count_down(n):
    while n > 0:
        yield n    # 每次执行到 yield 就暂停,交出 n
        n -= 1

for v in count_down(3):
    print(v)
# 输出:
# 3
# 2
# 1

它的执行流程值得单独说一遍:

  1. 调用 count_down(3) 时不执行任何代码,只拿到生成器。
  2. 第一次迭代(for 或 next()),代码从头执行到 yield n,交出 3 后原地暂停。
  3. 下一次迭代从暂停处继续:执行 n -= 1,再循环回 yield,交出 2……
  4. 函数自然结束时抛出 StopIteration,for 循环捕获它并正常收尾。

也就是说,yield 相当于给函数按下了"暂停键",下次从暂停处接着播。这正是它和 return 的本质区别:return 是结束,yield 是暂停。

惰性求值的经典场景:逐行读大文件

生成器最适合"数据流"式的处理,比如逐行读取一个很大的日志文件——不必把整个文件读进内存:

def read_lines(path):
    with open(path, encoding="utf-8") as f:
        for line in f:
            yield line.strip()  # 产出一行,暂停,等下一次要

# 内存中任何时刻只有当前这一行
for line in read_lines("access.log"):
    if "ERROR" in line:
        print(line)

再比如生成一个超大序列做统计,用生成器几乎不占额外内存:

total = sum(x * x for x in range(1_000_000))
print(total)  # 输出:333332833333500000

注意 sum() 里直接放生成器表达式,连括号都能省一层,这也是 Python 圈推崇的写法。

两个必须知道的特性

生成器是一次性的。 值被取走就没有了,不能回头重新遍历:

gen = (x for x in [1, 2, 3])
print(list(gen))   # 输出:[1, 2, 3]
print(list(gen))   # 输出:[](已经耗尽)

可以用 list() 随时把生成器"倒"成列表。 数据量不大、需要反复使用时,直接转列表更方便;数据量大、只过一遍时,保持生成器形态最省内存。

小结

  • 生成器是惰性产数据的对象:生成器表达式 (x*2 for x in ...) 与含 yield 的函数都能创建它。
  • yield 是暂停键:每次迭代执行到 yield 交出一个值并停住,下次从断点继续,函数结束抛 StopIteration。
  • 逐行读大文件、流式处理超大序列是生成器的经典场景,内存占用与数据总量无关。
  • 生成器只能遍历一次;需要反复使用就 list() 转成列表。