news 2026/9/9 22:13:04

CPython 生成器与协程实现剖析:从 PyGenObject、内嵌帧到字节码指令与 `yield from` 执行链

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CPython 生成器与协程实现剖析:从 PyGenObject、内嵌帧到字节码指令与 `yield from` 执行链

CPython 生成器与协程实现剖析:从 PyGenObject、内嵌帧到字节码指令与yield from执行链

【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython

CPython 将生成器(generator)与协程(coroutine)建立在同一个核心机制之上:一个持有内嵌解释器帧的特殊对象,借助专门的字节码实现“暂停—恢复”式执行。本文以 CPython 内部文档 InternalDocs/generators.md 为主线,结合genobject.cframe.cbytecodes.c的源码实现,讲透生成器对象的创建与销毁、迭代协议、yield from链式转发以及协程双向数据流的底层原理。读完本文,你将能够从“对象结构—帧生命周期—字节码语义”三个层面理解 CPython 中生成器与协程的真正执行方式,并能在后续深入阅读帧(InternalDocs/frames.md)与解释器(InternalDocs/interpreter.md)文档时建立清晰的上下文。

生成器与协程的实现模型:一个带内嵌帧的对象

CPython 中所有“可暂停恢复”的可调用对象——生成器迭代器(PyGen_Type)、协程(PyCoro_Type)、异步生成器(PyAsyncGen_Type)——共享同一个结构基元PyGenObject,它们都由一个帧 + 一组执行状态元数据组成。

_PyGenObject_HEAD:三类对象的公共头

生成器、协程、异步生成器对象的关键字段统一定义在宏_PyGenObject_HEAD(prefix)中,见 Include/internal/pycore_interpframe_structs.h:

字段说明
prefix##_weakreflist弱引用链表头
prefix##_name/prefix##_qualname生成器函数名与限定名(供gi_namegi_qualname等属性使用)
prefix##_exc_state_PyErr_StackItem该对象私有的“异常状态存储区”,用于挂起期间保存解释器异常状态
prefix##_origin_or_finalizer对协程保存cr_origin(创建点回溯),对异步生成器保存终结器aclose
prefix##_hooks_inited内省钩子(如审计/跟踪)是否已初始化
prefix##_closed是否已关闭(调用过close()
prefix##_running_async异步生成器专用:是否正在异步执行
prefix##_frame_state帧状态(见下文)
prefix##_iframe_PyInterpreterFrame内嵌的解释器帧

该宏分别以gicrag为前缀实例化出_PyGenObject_PyCoroObject_PyAsyncGenObject三个结构(Include/internal/pycore_interpframe_structs.h),公共接口侧的类型别名与类型对象则声明在 Include/cpython/genobject.h。

帧是生成器的一部分

与普通函数调用不同,生成器/协程的帧不分配在线程的帧栈上,而是作为_PyInterpreterFrame字段直接内嵌进对象结构(_PyGenObject_HEAD的最后两个字段)。这样生成器对象可以一次内存分配同时完成对象与帧的创建,这一设计与普通调用帧的分配方式(见 InternalDocs/frames.md 中的 Allocation 一节)形成对比:普通帧为了允许在调用激活结束后仍存活(如回溯、sys._getframe()),分配在 per-thread 的连续帧栈上;而生成器帧的生命周期天然与对象绑定,因此选择内嵌。

由于帧是内嵌的,帧与对象之间的双向换算可以靠固定偏移完成:

  • 由生成器取帧:&gen->gi_iframe(即&gen->gi_iframe);
  • 由帧取生成器:_PyGen_GetGeneratorFromFrame()通过offsetof(PyGenObject, gi_iframe)反推对象首地址,见 Include/internal/pycore_genobject.h。

_PyInterpreterFrame本身记录了正在执行的代码对象、instr_ptr(指令指针)、栈指针、frame_obj(对应的堆上PyFrameObject,可为NULL)、previous链接与owner归属等,其完整布局见 Include/internal/pycore_interpframe_structs.h。

帧状态机与归属

代码中围绕gi_frame_state的取值(如FRAME_CREATEDFRAME_EXECUTINGFRAME_CLEARED)和宏(FRAME_STATE_SUSPENDEDFRAME_STATE_FINISHED)体现了生成器帧的生命周期:对象刚创建时帧处于“已创建未启动”状态,send()恢复执行期间为“执行中”,遇到yield挂起后进入“已挂起”,函数运行完毕或对象被关闭、清除后进入“已清除”。此外_PyInterpreterFrame.owner字段(Include/internal/pycore_interpframe_structs.h)标明帧归属,其中FRAME_OWNED_BY_GENERATOR是生成器/协程内嵌帧的标记。

生成器对象的创建与销毁

创建:RETURN_GENERATOR指令

调用一个生成器函数时,其函数体字节码的开头是一条RETURN_GENERATOR指令(该指令在 Include/opcode_ids.h 中登记)。它的执行过程(实现于 Python/bytecodes.c)大致如下:

  1. 通过_Py_MakeCoro(func)创建一个生成器对象(协程也走同一创建路径),它已带有一个内嵌的_PyInterpreterFrame
  2. 当前执行帧复制到生成器的内嵌帧_PyFrame_Copy(frame, gen_frame)),于是生成器函数的局部变量、栈与代码状态就“搬家”进了新对象;
  3. 将内嵌帧的owner字段改写为FRAME_OWNED_BY_GENERATOR,并把gi_frame_state置为FRAME_CREATED
  4. 生成器对象的引用被压入栈,同时解释器弹出并销毁当前调用帧,返回生成器函数的调用方——此时函数调用已经“结束”,真正的工作等下次恢复才发生。

需要特别指出的是:由于生成器对象与帧是一次性分配的,RETURN_GENERATOR之后调用方栈上得到的是一个自成一体、随时可被恢复执行的对象,而不是一个仍在运行中的调用。

恢复执行:gen_send_ex2()_PyEval_EvalFrame()

当生成器被.send().next()for循环等途径恢复时,最终都会汇入核心函数gen_send_ex2()(Objects/genobject.c)。该函数要求生成器帧处于“执行中”状态,其关键动作包括:

  • 把传入参数arg压入内嵌帧的值栈顶部(对应yield表达式的求值结果);
  • 保存线程状态当前的exc_info,把tstate->exc_info切换到生成器私有的gi_exc_state,以便恢复后能找回挂起时保存的异常状态;
  • 调用_PyEval_EvalFrame(tstate, frame, exc)在内嵌帧上继续执行生成器函数。

解释器从帧返回后,通过线程状态中的generator_return_kind字段区分本次返回是“产出值”(GENERATOR_YIELD)还是“运行结束”(GENERATOR_RETURN)。前者的返回值会通过presult交给调用方,表示生成器又吐出一个元素;后者表示生成器已经耗尽,帧进入FRAME_CLEARED。从源码注释看,CPython 3.15 之前是用gi_frame_state承担这一区分职责的,改用独立字段是为了在不依赖 GIL 的构建下获得线程安全(该逻辑在 Objects/genobject.c 附近有明确说明)。

挂起时的关键动作:YIELD_VALUE

从生成器角度看,“产出”由YIELD_VALUE字节码实现。它与RETURN_VALUE有相似之处——都把栈顶的值带回调用方帧——但它还承担额外的簿记工作:

  • 更新帧的instr_ptr,使其指向yield之后的指令,这样下次恢复时能精确地从“挂起点之后”继续;
  • 把解释器当前的异常状态保存到生成器对象上(写回gi_exc_state),恢复时再复制回解释器状态。

从调用方的角度看,send()的调用语义类似于普通函数调用,但区别在于:普通函数每次调用都从函数开头执行并在RETURN_VALUE后一次性交还控制权;而生成器可以在任意多次send()调用中被反复恢复,每次都在新的yield处再次把执行权交还给调用方帧。

销毁:gen_dealloc()与帧的“所有权转移”

当生成器对象被回收时,gen_dealloc()(Objects/genobject.c)会清理弱引用、触发终结器逻辑、并最终清理其内嵌帧。对协程与异步生成器,还会先释放cr_origin_or_finalizer保存的创建点信息或终结器。

这里有一个至关重要的边界情况:如果生成器的帧已经暴露给了 Python 代码(例如通过回溯或sys._getframe()创建了对应的PyFrameObject),那么该PyFrameObject可能活得比生成器对象更久——而内嵌的_PyInterpreterFrame会随对象一起消失。解决方案在_PyFrame_ClearExceptCode()(Python/frame.c):

  1. 检查帧的frame_obj字段是否非空;
  2. 若非空且其指向的PyFrameObject引用计数大于 1(说明还有 Python 侧引用持有它),则调用take_ownership()
  3. take_ownership()(Python/frame.c)把内嵌帧拷贝PyFrameObject内部预置的_f_frame_data空间中,将新帧的owner置为FRAME_OWNED_BY_FRAME_OBJECT,并重建f_back链接链——帧的所有权由此从生成器移交给了帧对象;
  4. 之后生成器对象本体才继续走完清理流程(清空gi_exc_state、释放gi_name/gi_qualname等)。

这一机制与 InternalDocs/frames.md 中“帧对象(Frame objects)”一节的描述完全对应:它让每个激活在 Python 侧看起来像拥有一个持久堆帧,同时把运行时开销保持在很低水平。

迭代协议与生成器特化指令

在 Python 层,用for循环遍历生成器会触发迭代协议。编译出的FOR_ITER指令对栈顶迭代器调用__next__,并把结果压栈。为了让常见迭代器类型的遍历更快,解释器会对FOR_ITER特化(specialization)(特化机制总览见 InternalDocs/interpreter.md),其中就包括针对生成器的FOR_ITER_GEN

FOR_ITER_GEN的特别之处在于:它绕过了对__next__的 Python/C 调用,改为直接把生成器的帧入栈并从最近一次yield之后的那条指令恢复执行,实现上对应_FOR_ITER_GEN_FRAME(Python/bytecodes.c 附近的特化分支,FOR_ITER_GENSEND_GENSEND_ASYNC_GEN等并列于各自的特化族中)。省去一次方法调用既能降低开销,也使得“生成器链上的遍历”路径更加直接。

值得强调的语义细节是:生成器的__next__()本质上就是“向生成器发送None”。在 Objects/genobject.c 中,gen_iternext()gen_send_ex2()之间正是指向这种等价关系——它把生成器的两个入口(next()send(value))统一到了同一条恢复路径上,只是next()固定发送None

链式生成器:yield fromSEND指令

yield from表达式的设计目标,是让一个生成器高效地透传另一个可迭代对象(通常也是生成器)产出的序列,同时保持 send/throw 语义的链式贯通。它在字节码层面由SEND指令实现。

SEND的语义

SEND指令(Python/bytecodes.c)将操作数指向的“恢复地址”等信息随指令一起编码,执行时:

  • 把传给它的值(操作数arg之前经由栈传递的 send 值)压入被链生成器帧的值栈;
  • 在该生成器帧上设置异常状态(为可能抛入的异常做准备);
  • 恢复被链生成器的执行。

与普通CALL一样,SEND也需要为被调方记录“返回偏移”:当生成器最终return(而非yield)时,控制流要回到yield from之后的指令。_PyInterpreterFrame.return_offset字段在这里承担了关键作用——正如 InternalDocs/frames.md 中“Instruction Pointer”一节所解释的,SEND需要同时向生成器传递两个偏移:一个给RETURN、一个给YIELD,两者分别由opargreturn_offset携带。

SEND返回时,被链生成器产出的值位于栈顶,随后以YIELD_VALUE沿生成器链向上透传给最外层调用方。这样一个“SEND后跟YIELD_VALUE”的序列会在循环中反复执行,直到被链生成器以StopIteration异常宣告耗尽。

SEND同样拥有针对生成器/协程的特化SEND_GEN_SEND_GEN_FRAME先做类型与状态守卫,命中后直接把 send 值压入gen->gi_iframe、切换exc_info、设置return_offset并通过_PUSH_FRAME内联地进入被链帧(Python/bytecodes.c),从而避免走通用迭代器发送路径。

异常处理:CLEANUP_THROW

yield from的 send-yield 循环中还可能被异常打断,此时由CLEANUP_THROW指令(Python/bytecodes.c 附近)接管,它从栈上取出“被链子迭代器、上次发送值、待处理异常状态”等信息进行处理:

  • 若异常类型是StopIteration,说明被链生成器正常结束:其value字段所携带的值就是整个yield from表达式——同时也是该生成器close()流程所需的“返回值”;
  • 其他任何异常都会被CLEANUP_THROW重新抛出,继续沿外层传播。

因此,yield from对“正常产出 / 正常结束 / 异常中断”三种结局都有精确的字节码级处理,这也是为什么它可以安全地替代手写的“for x in sub: yield xStopIteration手动转译”模式。

协程:利用yield返回值的双向数据流

文档中给出了一个精炼的定义:协程就是会使用yield表达式返回值的生成器。所谓“返回值”,指的是唤醒它的那次.send()调用所携带的参数。由此形成双向数据通道:

  • 生成器 → 调用方:通过yield表达式的实参(产出的值);
  • 调用方 → 生成器:通过.send()调用传入的参数(yield表达式求值的结果)。

正是“yield表达式的求值结果等于下一次send的参数”这一规则,让生成器得以读取外部数据。而yield from会把调用方的 send 参数继续转发给被链的生成器,因此该数据流可以沿整条生成器链一直贯通(见gen_send_ex2()arg的处理逻辑,Objects/genobject.c)。

同时需要重申前面提到的统一性:由于__next__()只是self.send(None),所以生成器与协程在“恢复执行”这一底层机制上完全一致;区别只在于——协程会真正使用send传入的参数,而普通生成器即使收到了参数也通常忽略它。这也解释了为何文档将协程描述为“使用yield返回值的生成器”:async/await只是把这种双向通道语义与事件循环调度结合了起来,底层的暂停—恢复原语依然来自生成器机制。

总结与延伸阅读

从 CPython 实现的角度看,生成器/协程的本质可以浓缩为一句话:一个内嵌_PyInterpreterFrame的对象,配合一组专用字节码(RETURN_GENERATORYIELD_VALUESENDCLEANUP_THROWFOR_ITER_GEN),把普通函数调用“一次执行、一次返回”的模型扩展成“多次恢复、多次产出”的协作式执行模型。其关键实现取舍包括:

  • 帧内嵌于对象,保证“单次分配”与可独立存活的生命周期;
  • RETURN_GENERATOR负责创建对象并复制调用帧;
  • 挂起时更新指令指针并保存异常状态到对象上,恢复时再装回;
  • 帧对象逃逸出生成器生命周期时,通过take_ownership()拷贝帧并转移所有权;
  • 生成器特化(FOR_ITER_GENSEND_GEN)与yield fromSEND+YIELD_VALUE+CLEANUP_THROW)共同构成高效的链式转发管道。

如果你希望沿着本文的思路继续深入,推荐按以下顺序阅读仓库内部文档与源码:

  • 帧的分配、布局与帧对象机制:InternalDocs/frames.md、Include/internal/pycore_interpframe_structs.h、Python/frame.c;
  • 生成器对象各入口(send/throw/close/__next__)的完整语义与状态机细节:Objects/genobject.c、Include/cpython/genobject.h、Include/internal/pycore_genobject.h;
  • 相关字节码指令的权威定义与执行逻辑:Python/bytecodes.c 中RETURN_GENERATORYIELD_VALUESEND/SEND_GENFOR_ITER_GENCLEANUP_THROW各节;
  • 解释器特化与自适应字节码的宏观机制:InternalDocs/interpreter.md。

【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 22:10:20

把微信聊天记录存成网页、文档和表格:WeChatMsg免费工具上手教程

把微信聊天记录存成网页、文档和表格:WeChatMsg免费工具上手教程 【免费下载链接】WeChatMsg 提取微信聊天记录,将其导出成HTML、Word、CSV文档永久保存,对聊天记录进行分析生成年度聊天报告 项目地址: https://gitcode.com/GitHub_Trendin…

作者头像 李华
网站建设 2026/9/9 22:09:57

五子棋胜负判断:方向数组与矩阵遍历的核心解法

2023B卷的这道“五子棋迷”,我第一眼看到题目名字的时候,还以为是让写一个能自己下棋的AI。结果读完题面才发现,它只是让你判断一个已经摆好的棋盘上,黑棋还是白棋已经连成了五个子。题面本身不算复杂,但如果你没把矩阵…

作者头像 李华
网站建设 2026/9/9 22:09:38

NeMo Voice Agent 实战指南:3步在本地跑通全开源语音助手

NeMo Voice Agent 实战指南:3步在本地跑通全开源语音助手 【免费下载链接】Speech A scalable generative AI framework built for researchers and developers working on Large Language Models, Multimodal, and Speech AI (Automatic Speech Recognition and Te…

作者头像 李华
网站建设 2026/9/9 22:09:36

如何用 Ant Design CLI 离线查询组件 API、Demo 与 Design Token

如何用 Ant Design CLI 离线查询组件 API、Demo 与 Design Token 【免费下载链接】ant-design An enterprise-class UI design language and React UI library 项目地址: https://gitcode.com/GitHub_Trending/an/ant-design 在编写或维护基于 Ant Design(an…

作者头像 李华
网站建设 2026/9/9 22:07:09

OpenSim外部几何模型导入:从STL预处理到XML挂载全指南

简介:面向OpenSim生物力学建模初学者与研究人员,这份资源演示了在OpenSim 4.1环境下为leg6dof9musc腿部六自由度九肌肉模型添加外部几何模型的具体过程。压缩包共3个文件,包括原始OSIM模型文件、STL格式的示例外部几何体,以及用于…

作者头像 李华