一、生成器的准确定位:它不是"特殊列表",而是"惰性迭代器构造器"
生成器最准确的定义是:
生成器函数是包含 yield 的函数;调用它不会立刻执行函数体,而是返回一个生成器对象。这个对象实现了迭代器协议,可以在每次请求下一个值时继续执行,直到再次遇到 yield 或最终结束。
先看最基础的例子:
def count_up_to(n):
current = 1
while current <= n:
yield current
current += 1
gen = count_up_to(3)
print(gen) # <generator object count_up_to at ...>
print(next(gen)) # 1
print(next(gen)) # 2
print(next(gen)) # 3
这个例子里最重要的事实不是它输出了 1、2、3,而是下面两点:
- 调用 count_up_to(3) 时,函数体没有一次性执行完。
- 返回值不是列表,而是生成器对象。
如果把同样逻辑写成普通函数:
def count_up_to_list(n):
result = []
current = 1
while current <= n:
result.append(current)
current += 1
return result
data = count_up_to_list(3)
print(data) # [1, 2, 3]
这里的差异不是"语法不同"这么简单,而是计算模型不同:
- 列表方案是立即求值。
- 生成器方案是按需求值。
- 列表一次性占用完整结果的内存。
- 生成器只在迭代推进时产生当前值。
因此,生成器不是"另一种容器",而是一种延迟计算机制。
二、为什么 yield 会改变函数语义
普通函数的执行模型很简单:调用,执行到底,return 返回,栈帧销毁。
一旦函数体内出现 yield,这个函数就不再是普通函数,而会被编译为生成器函数。它的执行语义变成:
- 调用时不立即运行主体逻辑。
- 返回生成器对象。
- 每次被 next 或 for 驱动时,从上次暂停位置继续执行。
- 遇到 yield 暂停,并把值返回给外部。
- 执行完毕时抛出 StopIteration。
看一个更能说明"暂停/恢复"本质的例子:
def demo():
print("step 1")
yield "A"
print("step 2")
yield "B"
print("step 3")
gen = demo()
print(next(gen))
print(next(gen))
try:
print(next(gen))
except StopIteration:
print("generator finished")
运行顺序是:
- 第一次 next 时,打印 step 1,返回 A。
- 第二次 next 时,从上次 yield 之后继续,打印 step 2,返回 B。
- 第三次 next 时,继续执行,打印 step 3,然后结束并抛出 StopIteration。
这说明 yield 的核心作用不是"返回值",而是"挂起当前执行现场"。
三、生成器与迭代器的关系:生成器是迭代器的一种实现
理解生成器,不能绕开迭代器协议。Python 的迭代器协议要求对象具备两个特征:
- 有 iter 方法,返回迭代器自身或另一个迭代器。
- 有 next 方法,每次返回下一个值,没有值时抛出 StopIteration。
可以手写一个迭代器类:
class CountIterator:
def __init__(self, n):
self.n = n
self.current = 1
def __iter__(self):
return self
def __next__(self):
if self.current > self.n:
raise StopIteration
value = self.current
self.current += 1
return value
it = CountIterator(3)
for item in it:
print(item)
而用生成器实现同样逻辑:
def count_generator(n):
current = 1
while current <= n:
yield current
current += 1
for item in count_generator(3):
print(item)
两者的语义完全一致,但生成器版本更短、更接近问题本身。这里可以得到一个非常重要的结论:
生成器不是对迭代器协议的替代,而是对迭代器协议的高级语法封装。
也正因为如此,凡是能接受迭代器的地方,通常都能接受生成器:
total = sum(x for x in range(5))
print(total) # 10
四、生成器对象保存了什么状态
生成器之所以能暂停后恢复,是因为它不是简单保存"上一个值",而是保存整个执行现场。包括但不限于:
- 局部变量当前值。
- 指令执行位置。
- 当前 try、for、while 等控制流上下文。
- 必要的异常处理状态。
看一个例子:
def accumulate():
total = 0
for i in range(3):
total += i
yield total
gen = accumulate()
print(next(gen)) # 0
print(next(gen)) # 1
print(next(gen)) # 3
这里 total 会在多次恢复执行时持续存在。如果换成普通函数,函数返回后其局部状态早已消失;而生成器会在每次挂起时保留上下文。
可以把它理解成:
生成器对象是"携带执行现场的可恢复函数实例"。
这也是它比单纯"回调式逐个返回值"更强大的原因。
五、for 循环为什么天然支持生成器
很多人会用 next 手动驱动生成器,但实际工程里最常见的是 for。原因是 for 本质上就是迭代器协议的消费器。
例如:
def squares(n):
for i in range(n):
yield i * i
for value in squares(5):
print(value)
for 循环内部做的事情,本质上等价于:
gen = squares(5)
while True:
try:
value = next(gen)
print(value)
except StopIteration:
break
因此,生成器并不是"for 的特殊对象",而是因为它实现了迭代器协议,所以能被 for 自然消费。
这个认知非常关键,因为它意味着生成器不仅可以配合 for,也可以配合所有接受可迭代对象的标准库函数:
print(list(squares(5))) # [0, 1, 4, 9, 16]
print(tuple(squares(5))) # (0, 1, 4, 9, 16)
print(max(squares(5))) # 16
六、惰性计算是生成器最核心的工程价值
生成器的最大价值往往不是"语法优雅",而是惰性求值。所谓惰性求值,就是:
只有当调用方真正需要下一个值时,生成器才继续执行并产出该值。
看一个大数据场景:
def read_large_file(path):
with open(path, "r", encoding="utf-8") as f:
for line in f:
yield line.rstrip("\n")
for line in read_large_file("huge.log"):
if "ERROR" in line:
print(line)
如果这里返回列表,就意味着必须先把整个文件全部读入内存,再开始过滤。对于大文件,这会带来明显问题:
- 内存占用高。
- 首次结果出现慢。
- 无法做真正流式处理。
而生成器实现的是:
- 读一行。
- 处理一行。
- 继续下一行。
这类模式在日志处理、数据库游标消费、网络流、消息队列、爬虫管道、机器学习数据迭代中都非常常见。
再看列表方案与生成器方案的对比:
def even_squares_list(n):
result = []
for i in range(n):
if i % 2 == 0:
result.append(i * i)
return result
def even_squares_gen(n):
for i in range(n):
if i % 2 == 0:
yield i * i
print(even_squares_list(10))
print(list(even_squares_gen(10)))
小数据下两者输出相同,但规模扩大后,生成器的内存优势会越来越明显。
七、生成器只能遍历一次,这既是特性,也是限制
生成器是一次性消费对象。遍历结束后,不能自动重置。
示例:
def numbers():
for i in range(3):
yield i
gen = numbers()
print(list(gen)) # [0, 1, 2]
print(list(gen)) # []
第二次为空,不是 bug,而是因为生成器已经耗尽。
这与列表不同:
data = [0, 1, 2]
print(list(data)) # [0, 1, 2]
print(list(data)) # [0, 1, 2]
因此,工程上要区分两个问题:
- 我需要的是一次性流式消费,还是可重复访问的数据集合。
- 我需要的是节省内存,还是多次重用结果。
如果需要多次遍历,通常有三种选择:
- 重新创建生成器。
- 先把结果物化成列表或元组。
- 设计成返回"生成器工厂",而不是返回单个已创建的生成器。
例如:
def number_stream():
for i in range(3):
yield i
print(list(number_stream()))
print(list(number_stream()))
这才是可重复使用的形式,因为每次都重新创建了一个新生成器。
八、return 在生成器里意味着什么
普通函数中的 return 表示"返回某个结果并结束"。在生成器里,return 仍然表示结束,但它的语义更细:
- return 会终止生成器。
- 它不会像普通函数那样把值直接交给 for 循环。
- 如果写成 return value,这个值会附着在 StopIteration.value 上。
看例子:
def gen():
yield 1
yield 2
return "done"
g = gen()
print(next(g))
print(next(g))
try:
next(g)
except StopIteration as e:
print("stopped with:", e.value)
输出中,done 不会被 for 自动接收,但底层是可见的。
这说明一个重要事实:
生成器既可以"产出数据",也可以"最终返回一个结束值",只是这个结束值通常只有底层调用方才会关心。
这也正是后面理解 yield from 的关键前提。
九、send:生成器不只是产出值,也可以接收值
如果说普通迭代器是"单向流",那么生成器通过 send 获得了"双向通信"能力。
先看最小例子:
def receiver():
print("generator started")
value = yield "ready"
print("received:", value)
yield "finished"
gen = receiver()
print(next(gen)) # 启动生成器,得到 ready
print(gen.send(42)) # 向 yield 表达式送入 42
这里需要精确理解一句话:
yield 既能把值送给外部,也能作为表达式接收外部通过 send 传回来的值。
也就是说:
value = yield "ready"
这句分成两半理解:
- 先把 ready 产出给外部。
- 等下次恢复执行时,把 send 传入的参数赋给 value。
更完整的例子:
def accumulator():
total = 0
while True:
number = yield total
if number is None:
break
total += number
gen = accumulator()
print(next(gen)) # 0
print(gen.send(10)) # 10
print(gen.send(5)) # 15
try:
gen.send(None)
except StopIteration:
print("accumulator stopped")
这个例子展示了生成器的另一个身份:
它不仅可以是数据源,也可以是状态机。
十、为什么第一次 send 不能直接发送非 None 值
这是生成器使用中一个非常经典的细节。下面代码会报错:
def echo():
value = yield
print(value)
gen = echo()
gen.send(123)
原因是生成器刚创建时,尚未执行到第一个 yield 位置,因此没有"挂起点"可以接收外部值。必须先把它推进到第一个 yield。
正确写法:
def echo():
value = yield
print("received:", value)
gen = echo()
next(gen)
try:
gen.send(123)
except StopIteration:
pass
所以规则是:
第一次恢复生成器时,只能用 next(gen) 或 gen.send(None)。
这是语言语义,不是实现偶然。
十一、throw:向生成器内部注入异常
生成器不仅能接收正常数据,也能从外部接收异常。这个能力由 throw 提供。
示例:
def worker():
try:
yield 1
yield 2
except ValueError as e:
yield f"caught: {e}"
yield 3
gen = worker()
print(next(gen)) # 1
print(gen.throw(ValueError("bad"))) # caught: bad
print(next(gen)) # 3
这里发生的事情是:
- 生成器先产出 1。
- 外部不是继续 next,而是向挂起点抛入 ValueError。
- 生成器内部的 try/except 捕获该异常。
- 然后继续运行。
throw 的工程价值在于:它允许外部调度器把错误信号注入协作式执行流程中。虽然日常业务代码不常直接使用,但在早期协程框架、任务调度器、控制流库中非常重要。
十二、close:显式终止生成器
close 用于通知生成器应尽快结束。其本质是向生成器内部注入 GeneratorExit。
看例子:
def sample():
try:
yield 1
yield 2
finally:
print("cleaning up")
gen = sample()
print(next(gen))
gen.close()
输出会触发 finally 中的清理逻辑。
这说明生成器不仅是"会暂停的函数",还是"可管理生命周期的执行体"。当它内部持有文件句柄、网络连接、锁、事务上下文时,这一点尤其重要。
例如:
def read_lines(path):
f = open(path, "r", encoding="utf-8")
try:
for line in f:
yield line
finally:
print("closing file")
f.close()
如果外部提前停止消费,显式 close 可以让资源及时释放。
十三、yield from:真正的生成器委托机制
很多文章把 yield from 解释成"把子可迭代对象一个个 yield 出去",这只是表层现象。它更准确的定义是:
yield from 把当前生成器的控制权委托给另一个迭代器或生成器,并自动转发 next、send、throw、close,同时还能接收子生成器的最终返回值。
先看最简单的形式:
def sub():
yield 1
yield 2
def main():
yield 0
yield from sub()
yield 3
print(list(main())) # [0, 1, 2, 3]
如果不用 yield from,需要手写:
def main_manual():
yield 0
for item in sub():
yield item
yield 3
但这只是"值的转发"。yield from 的真正强大之处,在于它还能处理 return 值:
def sub():
yield 1
yield 2
return "sub finished"
def main():
result = yield from sub()
yield f"result was: {result}"
print(list(main()))
输出中最后一项会拿到子生成器 return 的结果。
这说明 yield from 不只是展开序列,而是在语言层面建立了一条"父生成器与子生成器之间的完整委托通道"。
十四、yield from 与手写 for 循环的本质区别
为了看清这个差异,比较两段代码。
第一种,手写 for:
def sub():
received = yield 1
yield f"sub got {received}"
def outer_manual():
for item in sub():
yield item
第二种,yield from:
def sub():
received = yield 1
yield f"sub got {received}"
def outer_delegate():
yield from sub()
对于 send、throw、close 来说,两者行为不同。yield from 会把这些操作自动继续传递给 sub;而手写 for 只是普通地把 next 拿到的值再 yield 出来,无法完整复现委托语义。
因此,严谨地说:
yield from 不是一个简写循环,而是生成器协议的组合器。
十五、生成器表达式:语法紧凑,但语义仍然是惰性迭代
生成器表达式写法与列表推导式很像:
squares_list = [x * x for x in range(5)]
squares_gen = (x * x for x in range(5))
print(squares_list) # [0, 1, 4, 9, 16]
print(squares_gen) # <generator object ...>
print(list(squares_gen)) # [0, 1, 4, 9, 16]
两者的关键差异不是括号,而是求值时机:
- 列表推导式会立即构造完整列表。
- 生成器表达式只在消费时逐项计算。
这在链式处理中很常见:
total = sum(x * x for x in range(1_000_000))
print(total)
这里如果写成列表推导式:
total = sum([x * x for x in range(1_000_000)])
通常就会额外创建一个庞大中间列表,而实际上 sum 并不需要一次拿到所有值。
所以一个常见实践是:
如果最终消费者是 sum、any、all、max、min、join 之外的某些支持迭代的聚合器,并且不需要中间结果复用,优先考虑生成器表达式。
十六、生成器管道:把复杂处理拆成一条数据流
生成器在工程上非常适合构建流式处理管道。每一层只做一件事,并通过 yield 向下游传递。
示例:读取日志、过滤错误、提取时间戳。
def read_lines(lines):
for line in lines:
yield line.strip()
def filter_errors(lines):
for line in lines:
if "ERROR" in line:
yield line
def extract_timestamps(lines):
for line in lines:
parts = line.split(" ", 1)
yield parts[0]
raw_lines = [
"2026-04-20 INFO startup",
"2026-04-20 ERROR database down",
"2026-04-20 ERROR timeout",
]
pipeline = extract_timestamps(filter_errors(read_lines(raw_lines)))
for item in pipeline:
print(item)
这种风格的优势是:
- 每层职责单一。
- 中间结果不必一次性物化。
- 可以自然拼接。
- 适合超大数据流。
如果用列表中间态实现,往往会写成:
- 先清洗成一个列表。
- 再过滤成另一个列表。
- 再映射成第三个列表。
这样会产生更多中间对象和内存压力。
十七、生成器适合表达有限流,也适合表达无限流
生成器并不要求必须有限。它非常适合描述概念上"没有终点"的序列。
例如斐波那契无限流:
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
gen = fibonacci()
for _ in range(10):
print(next(gen))
普通列表无法自然表示"无限序列",但生成器可以,因为它不需要提前构造所有值。
这也说明生成器非常适合以下场景:
- 实时数据源。
- 事件流。
- 概念上的无限序列。
- 外部驱动、随取随算的计算流。
当然,这也要求消费端必须有终止条件,否则会无限运行。
十八、生成器里的异常传播机制
生成器内部出现异常时,和普通函数一样会向外传播;不同之处在于,它可能在多次恢复执行中的某一次才抛出。
示例:
def broken():
yield 1
raise RuntimeError("something went wrong")
gen = broken()
print(next(gen))
try:
print(next(gen))
except RuntimeError as e:
print("caught:", e)
这说明生成器的错误并不是在创建对象时出现,而是在执行推进到相应位置时才暴露。
因此,在工程上要非常明确:
- 创建生成器对象通常不代表逻辑已成功。
- 真正的失败可能发生在消费阶段。
- 生成器 API 的调用方必须对迭代过程中的异常负责。
这个特性和数据库游标、网络流、延迟计算系统非常一致。
十九、生成器与上下文资源:不要忽视中途停止消费的问题
看一个例子:
def data_source():
print("open resource")
try:
for i in range(5):
yield i
finally:
print("close resource")
gen = data_source()
print(next(gen))
print(next(gen))
gen.close()
如果外部不再继续消费,但又没有让生成器正常结束,那么内部资源可能会延迟释放。因此写生成器时,凡是涉及资源管理,都应尽量使用 try/finally 或 with。
例如:
def read_file(path):
with open(path, "r", encoding="utf-8") as f:
for line in f:
yield line
这里即便消费提前结束,只要生成器被关闭或最终被回收,with 的退出逻辑都更清晰。
原则上讲:
生成器非常适合流式资源使用,但也要求开发者更认真处理生命周期。
二十、生成器不是并发模型,但它启发了协程模型
很多现代 Python 开发者接触协程时首先想到 async 和 await,但从语言演化史看,生成器是协程的重要前身。特别是 send、throw、close、yield from 这些能力,直接铺垫了后来的协程协议。
下面这个例子不是现代推荐写法,但能帮助理解生成器为什么被视为"协作式控制流"的基础:
def task():
print("task step 1")
yield
print("task step 2")
yield
print("task step 3")
t = task()
next(t)
next(t)
try:
next(t)
except StopIteration:
pass
这个任务不是线程,也不是进程,它只是一个可被调度器反复恢复的执行体。早期很多协程库就是基于这类思想构建的。
不过必须明确:
- 普通生成器不是现代异步编程的完整替代。
- 现代异步代码应优先使用 async def 和 await。
- 但理解生成器,有助于更深刻地理解协程为何能够暂停和恢复。
二十一、异步生成器:生成器思想在异步世界的延伸
Python 还提供了异步生成器,用于异步上下文中的流式产出。
示例:
import asyncio
async def async_counter():
for i in range(3):
await asyncio.sleep(0.1)
yield i
async def main():
async for item in async_counter():
print(item)
asyncio.run(main())
异步生成器与普通生成器的关系可以概括为:
- 普通生成器服务于同步迭代。
- 异步生成器服务于异步迭代。
- 一个通过 for 消费。
- 一个通过 async for 消费。
这说明生成器背后的思想非常稳定:
无论同步还是异步,本质都是"把序列生产改造成可暂停、可恢复、按需产出的过程"。
二十二、常见误区一:把生成器当成"性能一定更高"的方案
生成器经常能减少内存占用,但它并不意味着所有场景都更快。
看例子:
def list_version():
return [x * 2 for x in range(1000)]
def gen_version():
return (x * 2 for x in range(1000))
如果最终你马上就要把生成器全部转成列表:
data = list(gen_version())
那你仍然会物化整个结果,甚至还多了一层生成器调度开销。
所以应当严谨地说:
- 生成器通常更省内存。
- 生成器不保证绝对更快。
- 如果最终必须完整落地全部数据,列表有时反而更直接。
- 生成器最适合"边生产边消费"。
二十三、常见误区二:在调试时误以为生成器"没有执行"
初学者常见疑问是:
"我明明调用了函数,为什么函数体里的打印没执行?"
例子:
def demo():
print("running")
yield 1
gen = demo()
print("created")
只会打印 created,而不会打印 running。因为生成器函数被调用时只是创建生成器对象,没有推进执行。
真正执行是在:
print(next(gen))
之后才会打印 running。
因此,调试生成器时必须分清两个时刻:
- 创建时刻。
- 消费时刻。
很多"逻辑没生效"的误判,本质上只是没有触发消费。
二十四、常见误区三:把生成器返回给多个消费者共享
因为生成器是单次消费对象,所以把同一个生成器传给多个地方时,经常会出现"有人读完了,别人就没了"的问题。
示例:
def source():
for i in range(5):
yield i
gen = source()
print(next(gen)) # 0
print(list(gen)) # [1, 2, 3, 4]
print(list(gen)) # []
如果多个模块共享同一个生成器对象,就相当于共享一个会不断前进的游标。
解决方式通常是:
- 共享可重建的生成器工厂。
- 共享已经物化的数据。
- 明确约定只有单一消费者。
在接口设计上,这一点必须提前说明,否则非常容易引入隐蔽 bug。
二十五、生成器与列表推导式该如何选择
这个问题没有绝对答案,但有相对稳定的判断标准。
适合使用列表的场景
- 数据量不大。
- 需要多次遍历。
- 需要随机访问或切片。
- 需要立即看到完整结果。
示例:
values = [x * x for x in range(10)]
print(values[3])
print(values[-1])
适合使用生成器的场景
- 数据量大或可能无限。
- 只需要单次流式消费。
- 处理过程适合流水线。
- 希望尽快得到首批结果,而不是等待全部完成。
示例:
values = (x * x for x in range(10_000_000))
print(next(values))
print(next(values))
所以真正的选择依据不是"哪种语法更高级",而是数据访问模式。
二十六、工程上如何写出可维护的生成器
生成器很强,但也容易被写成难调试的"隐式控制流"。比较稳妥的实践包括:
1. 让每个生成器职责单一
def filter_positive(numbers):
for n in numbers:
if n > 0:
yield n
这种风格比一个生成器内部既过滤、又映射、又统计、又记录日志更容易维护。
2. 明确说明是否一次性消费
def load_records():
for i in range(3):
yield {"id": i}
调用方需要知道这是流,而不是静态集合。
3. 涉及资源时始终考虑 finally 或 with
def open_and_stream(path):
with open(path, "r", encoding="utf-8") as f:
for line in f:
yield line
4. 不要为"炫技"而滥用 send 和 throw
这些机制很强,但会显著增加认知负担。大多数业务代码只需要普通 yield 即可。
5. 对外接口尽量稳定
如果某个函数对调用方来说更像"数据集合",而不是"数据流",那么直接返回列表可能更符合语义。
二十七、一个完整案例:用生成器构建日志处理流水线
下面给出一个稍完整的工程化例子,展示生成器如何组合出清晰的数据处理管道。
def read_lines(lines):
for line in lines:
yield line.strip()
def parse_log(lines):
for line in lines:
parts = line.split(" ", 2)
if len(parts) == 3:
date, level, message = parts
yield {
"date": date,
"level": level,
"message": message,
}
def filter_level(records, target_level):
for record in records:
if record["level"] == target_level:
yield record
def format_messages(records):
for record in records:
yield f'{record["date"]}: {record["message"]}'
raw_data = [
"2026-04-20 INFO startup complete",
"2026-04-20 ERROR database unavailable",
"2026-04-20 WARNING retrying",
"2026-04-20 ERROR timeout happened",
]
pipeline = format_messages(
filter_level(
parse_log(
read_lines(raw_data)
),
"ERROR"
)
)
for item in pipeline:
print(item)
这个例子体现出生成器在工程中的几个核心价值:
- 每一层输入输出都统一为可迭代流。
- 数据处理过程自然分层。
- 中间结果无需全部落地。
- 上游和下游耦合度低。
这就是生成器真正擅长的场景:面向流,而不是面向块。
二十八、从语言设计角度再看生成器
如果从更抽象的角度总结,生成器解决的是一个经典问题:
如何把"一个会逐步产生结果的过程",表达成"一个可被统一消费的对象"。
传统函数更适合表达"输入一次,输出一次"的映射关系;而现实世界里大量问题并不是这样:
- 文件是一行一行读的。
- 网络响应是一块一块到的。
- 日志是一条一条处理的。
- 事件是一件一件发生的。
- 无限序列理论上没有完整终点。
生成器的优雅之处在于,它没有发明一套完全陌生的新模型,而是在普通函数基础上,只通过 yield 就把"过程"改造成了"可恢复的迭代器"。
这也是为什么生成器一直是 Python 最具代表性的语言特性之一。
结论
生成器的本质,不是"更省内存的列表替代品",而是"把逐步计算过程对象化、迭代化、惰性化"的语言机制。理解生成器,应当把握以下几个层次:
- 语法层面:函数中出现 yield,就会变成生成器函数。
- 对象层面:调用生成器函数得到的是生成器对象,它实现了迭代器协议。
- 执行层面:生成器可以暂停和恢复,保存完整执行现场。
- 通信层面:除了 next 取值,还可以用 send、throw、close 进行控制。
- 组合层面:yield from 提供完整委托机制。
- 工程层面:生成器最适合流式处理、大数据迭代、无限序列和处理管道。
- 边界层面:它是一次性消费对象,不适合所有场景,也不天然保证更快。
如果只记一句话,最值得记住的是:
生成器让"计算结果"不再必须一次性存在,而可以作为"按需推进的过程"被消费。