yield 是 Python 中一个关键的关键字,它让一个普通函数转变为生成器函数 ,调用时返回一个生成器对象。生成器对象实现了迭代器协议,支持惰性求值,可以暂停执行并在之后恢复,从而实现高效的流式处理、协程等功能。
1. 从普通函数到生成器函数
当一个函数体中包含 yield 语句时,Python 解释器不会把它当作普通函数处理:
-
普通函数 :调用时执行函数体,遇到
return返回值,函数栈帧在返回后被销毁。 -
生成器函数 :调用时不执行函数体 ,而是立即返回一个生成器对象 。函数体实际执行发生在对生成器对象调用
__next__()(或next())时。pythondef my_gen(): print("start") yield 1 print("resume") yield 2 g = my_gen() # 不输出任何内容,返回生成器对象 next(g) # 输出 "start",得到 1 next(g) # 输出 "resume",得到 2 next(g) # 抛出 StopIteration
2. 生成器如何保存状态
生成器能够"暂停"和"恢复"的秘密在于它的执行状态被保存。
-
当生成器执行到
yield时,当前栈帧(包含局部变量、指令指针、异常状态等)被从调用栈上弹出,但不会被销毁,而是保存在生成器对象的内部。 -
下一次调用
next()时,生成器将之前保存的栈帧重新压入调用栈,并从yield之后的下一条指令继续执行。
这种机制使得生成器可以记住所有局部变量、循环状态等,就像从未中断过一样。
在 CPython 实现中,生成器对象有一个 gi_frame 属性指向栈帧对象,该栈帧在堆上分配,因此可以独立于调用栈生存。
3. 迭代器协议
生成器对象是迭代器,因为它实现了:
-
__iter__():返回自身。 -
__next__():恢复执行,直到遇到下一个yield并返回其值,如果没有更多值则抛出StopIteration。
因此生成器可以直接用在 for 循环等期望迭代器的地方。
python
def even_until(n):
i = 0
while i <= n:
yield i
i += 2
for x in even_until(10):
print(x) # 0 2 4 6 8 10
4. yield 的增强功能:send()、throw()、close()
生成器不仅仅是迭代器,还可以作为协程 使用,通过 yield 表达式接收外部传入的值。
-
send(value):恢复生成器,并将value作为yield表达式的返回值。第一次启动生成器必须用next()或send(None)。 -
throw(type, value=None, traceback=None):在生成器暂停点抛出异常。 -
close():向生成器内部抛出GeneratorExit异常,用于清理资源。def coroutine(): x = yield 0 print(f"received: {x}")
c = coroutine() next(c) # 运行到 yield,返回 0 c.send(42) # 将 42 赋给 x,打印 "received: 42",然后生成器结束
5. 内存与性能优势
-
惰性求值:生成器按需生成值,不需要一次性将所有数据载入内存,适合处理大文件、无限序列等。
-
栈帧复用:生成器内部栈帧在堆上分配,生命周期与生成器对象绑定,避免了普通函数多次调用带来的栈分配/释放开销(尽管每次恢复仍有开销,但总体更适合流式场景)。
6. 底层实现简述
在 CPython 中,生成器对象是 PyGenObject 类型,主要字段包括:
-
gi_frame:指向PyFrameObject,保存执行状态。 -
gi_code:指向函数代码对象。 -
gi_running:表示生成器是否正在执行(防止递归调用)。
当一个生成器被 next() 时,解释器通过 _PyEval_EvalFrameDefault() 恢复执行栈帧。遇到 yield 时,将返回值存入生成器对象的 gi_exc_state,然后保存当前栈帧状态,并切换回调用者。
7. 总结
-
yield使函数成为生成器函数,调用返回生成器对象。 -
生成器对象保存了执行状态(栈帧),可以在多次
next()之间暂停与恢复。 -
生成器实现了迭代器协议,并支持
send()、throw()、close()等协程特性。 -
核心优势是惰性求值与低内存占用,适用于处理大数据流、无限序列以及实现轻量级协程。
理解 yield 的原理,有助于编写高效、可读的流式代码,也能更深入地理解 Python 的迭代器和协程模型。最近在搞python ai agent这条路,基础知识要打扎实,想输出记录一下,如果有错的地方欢迎指正~