跳到主要内容

生成器

生成器(generator)是一种按需产生数据的迭代器。它不会在创建时计算全部结果,而是在每次需要下一个值时继续执行,因此特别适合处理大数据、无限序列和数据管道。

本文示例适用于 Python 3。学完后,你应该能够:

  • 使用 yield 编写生成器函数;
  • 理解生成器暂停、恢复和一次性消费的特性;
  • 区分生成器函数、生成器对象和生成器表达式;
  • 使用 send()、throw()、close() 和 yield from 完成进阶控制;
  • 根据内存、复用和可读性需求选择生成器或列表。

为什么需要生成器​

假设要生成一百万个数字。直接创建列表会立即分配存放一百万个元素的空间:

numbers = [number * number for number in range(1_000_000)]

如果只需要逐个求和,可以让数字按需产生:

total = sum(number * number for number in range(1_000_000))

生成器把“如何产生下一个值”的逻辑保存下来,而不是保存所有结果。它降低了峰值内存,但不代表计算免费:每个值仍然需要在消费时计算。

用 yield 编写生成器函数​

函数体中只要出现 yield,调用它时就不会立即执行函数体,而是返回一个生成器对象:

def count_up_to(limit):
number = 1
while number <= limit:
yield number
number += 1


counter = count_up_to(3)
print(counter) # 一个生成器对象
print(next(counter)) # 1
print(next(counter)) # 2
print(next(counter)) # 3

再次调用 next(counter) 会抛出 StopIteration。通常让 for 自动处理结束信号:

for number in count_up_to(3):
print(number)

yield 会把当前值交给调用方,并暂停函数。下一次调用 next() 时,函数从 yield 后面继续运行:

def demo():
print("开始")
yield "第一步"
print("继续")
yield "第二步"
print("结束")


generator = demo()
print("尚未执行函数体")
print(next(generator)) # 打印“开始”,再得到“第一步”
print(next(generator)) # 打印“继续”,再得到“第二步”

生成器对象保存局部变量和暂停位置,所以每次恢复时可以接着上次的状态工作。

return 与生成器结束​

生成器中的 return 表示不再产生值。若带有返回值,该值会放在 StopIteration.value 中:

def generate_numbers():
yield 1
yield 2
return "完成"


generator = generate_numbers()
print(next(generator)) # 1
print(next(generator)) # 2
try:
next(generator)
except StopIteration as error:
print(error.value) # 完成

在普通 for 循环中,StopIteration.value 会被自动忽略。使用 yield from 委托子生成器时,返回值可以被接收。

生成器表达式​

简单场景可以使用生成器表达式:

even_numbers = (number for number in range(10) if number % 2 == 0)

print(next(even_numbers)) # 0
print(list(even_numbers)) # [2, 4, 6, 8]

它和列表生成式的区别在于括号:

list_result = [number * 2 for number in range(3)]
generator_result = (number * 2 for number in range(3))

前者创建完整列表,后者返回生成器。传给函数时,外层括号通常可以省略:

total = sum(number * 2 for number in range(3))

生成器是一次性的​

生成器只能从当前位置向前消费,不能索引、倒退或自动重置:

generator = (number for number in range(3))
print(list(generator)) # [0, 1, 2]
print(list(generator)) # []

需要多次遍历时,可以重新调用生成器函数,或显式保存为列表:

def numbers():
return (number for number in range(3))


first = list(numbers())
second = list(numbers())

不要把已消费的生成器传给多个需要完整数据的函数,否则后面的函数可能只得到剩余数据甚至空结果。

用生成器构建数据管道​

多个生成器可以串联,前一个生成器的输出成为后一个生成器的输入:

def read_values():
for text in ["10", "skip", "20", "30"]:
yield text


def parse_numbers(values):
for text in values:
if text.isdigit():
yield int(text)


def double(values):
for value in values:
yield value * 2


pipeline = double(parse_numbers(read_values()))
print(list(pipeline)) # [20, 40, 60]

在 list(pipeline) 被调用前,管道中的函数都不会真正处理数据。每取一个结果,调用链只推进到能产生下一个值的位置。

使用 yield from 委托迭代​

yield from iterable 会把子可迭代对象中的值逐个转发给调用方:

def combined():
yield from [1, 2]
yield from range(3, 5)


print(list(combined())) # [1, 2, 3, 4]

它比手写内部循环更简洁,还能正确转发 send()、异常和子生成器的返回值:

def sub_generator():
yield "工作中"
return 42


def main_generator():
result = yield from sub_generator()
yield f"结果是 {result}"


print(list(main_generator())) # ['工作中', '结果是 42']

向生成器发送数据:send()​

yield 既可以产生值,也可以接收调用方发送的值。第一次启动生成器时必须使用 next() 或 send(None),因为它还没有暂停在某个 yield 上:

def accumulator():
total = 0
while True:
value = yield total
if value is None:
return
total += value


generator = accumulator()
print(next(generator)) # 0,启动并停在 yield
print(generator.send(10)) # 10
print(generator.send(5)) # 15

send(value) 会让暂停处的 yield 表达式得到 value,然后继续运行到下一个 yield。这种协作式通信很灵活,但复杂度也高;普通转换管道通常直接传递参数即可。

throw() 和 close()​

throw() 会在生成器暂停处注入一个异常,close() 会注入 GeneratorExit,请求生成器结束:

def managed():
try:
yield "资源已准备"
finally:
print("清理资源")


generator = managed()
print(next(generator))
generator.close() # 输出“清理资源”

即使生成器提前关闭,也应在 finally 中释放文件、锁或其他资源。不要捕获 GeneratorExit 后继续无休止地产生值,这会导致 RuntimeError。

生成器与文件、网络流​

生成器适合把大文件逐行转换:

def error_lines(path):
with open(path, encoding="utf-8") as file:
for line in file:
if "ERROR" in line:
yield line.rstrip("\n")

with 会在生成器结束或关闭时释放文件。调用方如果只消费一部分后就丢弃生成器,引用计数或垃圾回收最终会触发清理,但对关键资源应使用显式的上下文管理设计,不要依赖回收时机。

生成器函数是同步协议;异步数据源应使用 async def 和 async for 的异步生成器,不能直接混用。

性能和选择​

生成器的优势:

  • 惰性计算,避免一次创建全部结果;
  • 可以表示无限序列;
  • 便于把多个处理步骤连接成流式管道。

生成器的代价:

  • 只能顺序访问,通常不能索引和重复遍历;
  • 调试时不容易直接看到所有结果;
  • 每次 next() 都有调度和函数恢复开销,小数据不一定更快。

若结果需要排序、切片、多次遍历或随机访问,应在合适的位置物化为列表:

values = list(generator)
values.sort()

常见错误​

把生成器函数当成结果​

调用带 yield 的函数得到的是生成器对象,不是列表。需要查看结果时,使用 list()(注意这会一次消费全部数据)或用 for 逐项处理。

在生成器中误用 return 产生数据​

return value 会结束生成器,不能像 yield value 那样产生一个普通迭代项。

忽略异常会结束生成器​

生成器内部未处理的异常会传递给调用方,并使生成器终止。对可跳过的坏数据,应在生成器内部明确捕获并记录。

把所有逻辑都塞进一个生成器​

过长的生成器难以测试。按读取、解析、过滤、转换等职责拆分,再用管道组合,通常更清楚。

练习​

  1. 编写生成器函数,依次产生斐波那契数列的前 n 项。
  2. 编写生成器,逐行读取文件并跳过空白行。
  3. 用生成器表达式计算 1 到 100 中所有偶数的平方和。
  4. 使用 yield from 合并两个生成器函数的结果。
  5. 修改累加器示例,使它收到 None 时优雅结束,并在 finally 中输出清理信息。

小结​

  • 生成器是按需提供数据的迭代器,函数中使用 yield 即可定义生成器函数。
  • 调用生成器函数不会立即执行;每次 next() 从上次暂停的 yield 继续。
  • 生成器表达式使用圆括号,适合简单的惰性计算。
  • 生成器通常只能顺序消费一次;需要复用时重新创建或物化结果。
  • yield from 可委托子迭代器,send()、throw()、close() 提供进阶控制。
  • 惰性降低内存峰值,但会牺牲随机访问和重复遍历能力,应按场景选择。