生成器
生成器(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 那样产生一个普通迭代项。
忽略异常会结束生成器
生成器内部未处理的异常会传递给调用方,并使生成器终止。对可跳过的坏数据,应在生成器内部明确捕获并记录。
把所有逻辑都塞进一个生成器
过长的生成器难以测试。按读取、解析、过滤、转换等职责拆分,再用管道组合,通常更清楚。
练习
- 编写生成器函数,依次产生斐波那契数列的前
n项。 - 编写生成器,逐行读取文件并跳过空白行。
- 用生成器表达式计算
1到100中所有偶数的平方和。 - 使用
yield from合并两个生成器函数的结果。 - 修改累加器示例,使它收到
None时优雅结束,并在finally中输出清理信息。
小结
- 生成器是按需提供数据的迭代器,函数中使用
yield即可定义生成器函数。 - 调用生成器函数不会立即执行;每次
next()从上次暂停的yield继续。 - 生成器表达式使用圆括号,适合简单的惰性计算。
- 生成器通常只能顺序消费一次;需要复用时重新创建或物化结果。
yield from可委托子迭代器,send()、throw()、close()提供进阶控制。- 惰性降低内存峰值,但会牺牲随机访问和重复遍历能力,应按场景选择。