CPython 生成器对象 C API 全解:PyGenObject、类型检查、PyGen_GetCode 与 3.16 起废弃的构造接口
CPython 用 Generator Objects 实现生成器迭代器,这是 C 扩展与解释器内部交互的核心对象之一。本篇基于 CPython 官方文档 Doc/c-api/gen.rst,结合 Include/cpython/genobject.h 与 Objects/genobject.c 的源码实现,完整讲清生成器对象的 C 接口面:类型对象、PyGen_Check / PyGen_CheckExact 检查宏、PyGen_GetCode 强引用语义、PyGenObject 内部结构布局,以及 PyGen_New、PyGen_NewWithQualName、PyAsyncGen_New 等构造函数为何在 3.16 起被标记废弃、并计划在 3.18 移除。读完后,你可以在 C 扩展中正确识别和检视生成器对象,并理解异步生成器(PEP 525)相关 API 的边界与限制。
生成器对象的来源与类型体系
文档开宗明义:生成器对象是 CPython 实现生成器迭代器的载体,它们通常由“迭代一个会 yield 值(或 await 其他 awaitable 的 async def 函数)”隐式创建,而不是通过显式调用 C API 构造:
#include <Python.h>
#include <cpython/genobject.h> // PyGen_Type, PyGen_Check 等
PyObject *gen; // 假设来自 Python 层调用传入的生成器
int is_gen = PyGen_Check(gen); // 是生成器(含子类)吗?
int is_exact = PyGen_CheckExact(gen); // 类型恰好是 PyGen_Type 吗?
C 接口面由两个核心符号构成:
PyGenObject:生成器对象的 C 结构体(不透明类型,public 头文件只声明typedef struct _PyGenObject PyGenObject;);PyTypeObject PyGen_Type:与生成器对象对应的类型对象。
两者在 Include/cpython/genobject.h 中声明。需要特别注意:该头文件整体包裹在 #ifndef Py_LIMITED_API 中(见 Include/cpython/genobject.h),即生成器对象接口属于非受限(full)C API,依赖稳定 ABI 的扩展在 Py_LIMITED_API 下拿不到这些符号。
从同一个头文件还能看到,生成器家族实际共享一套接口模式,头文件按“Generators / PyCoroObject / Asynchronous Generators”三段组织(Include/cpython/genobject.h):协程(PyCoro_Type、PyCoro_New)与异步生成器(PyAsyncGen_Type、PyAsyncGen_New)的声明就紧挨着生成器,这正对应源码中三类对象复用同一段结构前缀的事实。
类型检查:PyGen_Check 与 PyGen_CheckExact
文档定义了两个检查函数,语义不同:
| 接口 | 语义 | 要求 |
|---|---|---|
int PyGen_Check(PyObject *ob) |
ob 是否为生成器对象(包括子类) |
ob 不得为 NULL;本函数总是成功 |
int PyGen_CheckExact(PyObject *ob) |
ob 的类型是否恰好是 PyGen_Type |
ob 不得为 NULL;本函数总是成功 |
在当前仓库中,这两个接口并非 C 函数,而是头文件中的宏(Include/cpython/genobject.h):
#define PyGen_Check(op) PyObject_TypeCheck((op), &PyGen_Type)
#define PyGen_CheckExact(op) Py_IS_TYPE((op), &PyGen_Type)
这一实现区分了两种检查策略:
PyGen_Check走PyObject_TypeCheck,沿 MRO 查找,因此types.GeneratorType的子类实例也会返回真;PyGen_CheckExact走Py_IS_TYPE的精确类型比较,O(1) 且只认PyGen_Type本身。
文档标注“always succeeds”意味着这两个检查不会因对象类型本身而失败,扩展代码可以安全地用它作为分支条件(但仍须保证入参非 NULL)。
PyGen_GetCode:获取生成器包装的代码对象
PyCodeObject *PyGen_GetCode(PyGenObject *gen);
文档承诺:返回 gen 所包装的代码对象的一个新强引用(strong reference),且本函数总是成功。拿到后必须 Py_DECREF。
源码实现非常直接,位于 Objects/genobject.c:
/* Returns a borrowed reference */
static inline PyCodeObject *
_PyGen_GetCode(PyGenObject *gen) {
return _PyFrame_GetCode(&gen->gi_iframe);
}
PyCodeObject *
PyGen_GetCode(PyGenObject *gen) {
assert(PyGen_Check(gen));
PyCodeObject *res = _PyGen_GetCode(gen);
Py_INCREF(res);
return res;
}
可以看到:
- 内部辅助函数
_PyGen_GetCode返回借用引用,直接从内嵌帧gen->gi_iframe读取代码对象; - 公共 API
PyGen_GetCode在其上做Py_INCREF,兑现“新强引用”的承诺; - 入口处
assert(PyGen_Check(gen))保证传入的是生成器(或其子类)对象,与文档的入参类型约定一致。
典型用法是检查生成器来源代码的属性,例如:
PyCodeObject *code = PyGen_GetCode(gen);
if (code != NULL) {
/* 检查 co_name、co_filename、co_flags 等 */
Py_DECREF(code);
}
由于生成器的代码对象始终存在,文档才敢标注“always succeeds”——这与“可能分配失败”的构造函数有本质区别。
PyGenObject 的内部结构布局
PyGenObject 在 public 头文件中是不透明的,但仓库内部头 Include/internal/pycore_interpframe_structs.h 给出了真实布局:
/* _PyGenObject_HEAD defines the initial segment of generator
and coroutine objects. */
#define _PyGenObject_HEAD(prefix) \
PyObject_HEAD \
PyObject *prefix##_weakreflist; \
PyObject *prefix##_name; \
PyObject *prefix##_qualname; \
_PyErr_StackItem prefix##_exc_state; \
PyObject *prefix##_origin_or_finalizer; \
int8_t prefix##_hooks_inited; \
int8_t prefix##_closed; \
int8_t prefix##_running_async; \
int8_t prefix##_frame_state; \
_PyInterpreterFrame prefix##_iframe; \
struct _PyGenObject {
/* The gi_ prefix is intended to remind of generator-iterator. */
_PyGenObject_HEAD(gi)
};
从源码结构看,几个字段值得注意:
gi_name/gi_qualname:即 Python 层的__name__与__qualname__,构造时若不显式指定,会回退取代码对象的co_name/co_qualname(见下文构造流程);gi_iframe:内嵌的_PyInterpreterFrame,生成器的“挂起状态”直接住在结构体里,而非独立堆对象。内部工具_PyGen_GetGeneratorFromFrame(Include/internal/pycore_genobject.h)甚至能用offsetof(PyGenObject, gi_iframe)从帧指针反推出生成器对象,说明帧被刻意放在对象头部以支持这种指针运算;gi_frame_state:记录帧当前所处阶段(创建、运行、挂起于yield/yield from等)。在无 GIL 构建(Py_GIL_DISABLED)下,状态迁移会用原子 CAS 完成,参见 Objects/genobject.c 的gen_try_set_frame_state;- 同一段
_PyGenObject_HEAD宏还被PyCoroObject(cr_前缀)与PyAsyncGenObject(ag_前缀)复用,这正是三类对象共享大部分方法表与遍历/清理逻辑的根源。
gi_ 前缀的命名动机在源码注释中说明得很清楚:“generator-iterator(生成器迭代器)”。
构造函数:PyGen_New 与 PyGen_NewWithQualName(3.16 废弃,3.18 移除)
文档中的两个构造接口:
PyObject *PyGen_New(PyFrameObject *frame)——基于帧创建生成器;PyObject *PyGen_NewWithQualName(PyFrameObject *frame, PyObject *name, PyObject *qualname)——额外指定__name__与__qualname__。
两者都要求 frame 非 NULL,且对 frame 的引用被函数“窃取(stolen)”,出错时也一样——调用者交出引用后无论成功失败都不得再释放。
但文档对二者都加了明确的版本标注(Doc/c-api/gen.rst):
.. deprecated-removed:: 3.16 3.18 This function has not been used since 3.10. It is also impossible to construct a proper frame object to call this function.
当前仓库(Include/patchlevel.h 显示版本为 3.16.0a0)中,两个函数在头文件里已带 Py_DEPRECATED(3.16) 标记(Include/cpython/genobject.h):
Py_DEPRECATED(3.16) PyAPI_FUNC(PyObject *) PyGen_New(PyFrameObject *);
Py_DEPRECATED(3.16) PyAPI_FUNC(PyObject *) PyGen_NewWithQualName(PyFrameObject *,
PyObject *name, PyObject *qualname);
“自 3.10 起未被内部使用、且无法构造出合适的 frame 对象来调用它”这句话,对应了 CPython 帧对象内部表示的重构:PyFrameObject 的帧数据不再暴露可公开构造的内部结构,扩展层无法再手工组装一个合法的 PyFrameObject 传入。也就是说,这不是“功能没做完”,而是调用前提在架构上已不可能满足。
不过这两个函数的实现仍然保留在源码中(供解释器内部及过渡期使用),核心逻辑是 gen_new_with_qualname(Objects/genobject.c):
static PyObject *
gen_new_with_qualname(PyTypeObject *type, PyFrameObject *f,
PyObject *name, PyObject *qualname)
{
PyCodeObject *code = _PyFrame_GetCode(f->f_frame);
int size = code->co_nlocalsplus + code->co_stacksize;
PyGenObject *gen = PyObject_GC_NewVar(PyGenObject, type, size);
if (gen == NULL) {
Py_DECREF(f); /* 出错时也窃取 frame 的引用 */
return NULL;
}
/* Copy the frame */
_PyInterpreterFrame *frame = &gen->gi_iframe;
_PyFrame_Copy((_PyInterpreterFrame *)f->_f_frame_data, frame);
gen->gi_frame_state = FRAME_CREATED;
frame->owner = FRAME_OWNED_BY_GENERATOR;
Py_DECREF(f);
...
if (name != NULL)
gen->gi_name = Py_NewRef(name);
else
gen->gi_name = Py_NewRef(_PyGen_GetCode(gen)->co_name);
if (qualname != NULL)
gen->gi_qualname = Py_NewRef(qualname);
else
gen->gi_qualname = Py_NewRef(_PyGen_GetCode(gen)->co_qualname);
_PyObject_GC_TRACK(gen);
return (PyObject *)gen;
}
PyObject *
PyGen_NewWithQualName(PyFrameObject *f, PyObject *name, PyObject *qualname)
{
return gen_new_with_qualname(&PyGen_Type, f, name, qualname);
}
PyObject *
PyGen_New(PyFrameObject *f)
{
return gen_new_with_qualname(&PyGen_Type, f, NULL, NULL);
}
实现要点与文档语义一一对应:
- 可变大小 GC 对象:用
PyObject_GC_NewVar按co_nlocalsplus + co_stacksize分配,本地变量与操作数栈直接追加在生成器结构体尾部,避免额外堆分配; - 帧拷贝而非帧转移:
_PyFrame_Copy把PyFrameObject的帧数据拷入gen->gi_iframe,随后将帧owner置为FRAME_OWNED_BY_GENERATOR并设置gi_frame_state = FRAME_CREATED; - name/qualname 回退规则:不显式传入时取代码对象的
co_name/co_qualname——这就是 Python 层gen.__name__的默认来源; - 引用窃取语义:分配失败路径上先
Py_DECREF(f)再返回NULL,成功路径在拷贝完成后Py_DECREF(f),无论成败,f的引用都不再属于调用者,与文档的 “stolen (even on error)” 完全一致。
对扩展作者的实践含义:不要在自己代码中调用这三个构造函数(PyGen_New、PyGen_NewWithQualName、同族被废弃的 PyCoro_New),编译器也会用 Py_DEPRECATED(3.16) 给出警告;3.18 起它们将彻底消失,届时编译失败会成为硬性约束。
异步生成器对象(PEP 525)
文档后半部分对应 PEP 525 定义的异步生成器(async def 中 async for / async yield 语法),接口面为:
PyTypeObject PyAsyncGen_Type(3.6 加入):异步生成器的类型对象,在 Python 层对应types.AsyncGeneratorType;PyObject *PyAsyncGen_New(PyFrameObject *frame, PyObject *name, PyObject *qualname)(3.6 加入):语义与PyGen_NewWithQualName相同——设置__name__/__qualname__,窃取frame的引用且出错时同样窃取;成功返回新对象的强引用,失败返回NULL并设置异常。它同样带.. deprecated-removed:: 3.16 3.18标注,理由与同步版一致(Doc/c-api/gen.rst);int PyAsyncGen_CheckExact(PyObject *op)(3.6 加入):精确判断op是否为异步生成器对象,总是成功。
在头文件中的落地形式(Include/cpython/genobject.h):
typedef struct _PyAsyncGenObject PyAsyncGenObject;
PyAPI_DATA(PyTypeObject) PyAsyncGen_Type;
PyAPI_DATA(PyTypeObject) _PyAsyncGenASend_Type;
Py_DEPRECATED(3.16) PyAPI_FUNC(PyObject *) PyAsyncGen_New(PyFrameObject *,
PyObject *name, PyObject *qualname);
#define PyAsyncGen_CheckExact(op) Py_IS_TYPE((op), &PyAsyncGen_Type)
其实现与同步生成器共用 gen_new_with_qualname,只是传入的类型不同,并额外初始化异步专属字段(Objects/genobject.c):
PyObject *
PyAsyncGen_New(PyFrameObject *f, PyObject *name, PyObject *qualname)
{
PyAsyncGenObject *ag;
ag = (PyAsyncGenObject *)gen_new_with_qualname(&PyAsyncGen_Type, f,
name, qualname);
if (ag == NULL) {
return NULL;
}
ag->ag_origin_or_finalizer = NULL; /* 关闭钩子(aclose hook)挂点 */
ag->ag_closed = 0;
ag->ag_hooks_inited = 0;
ag->ag_running_async = 0;
return (PyObject*)ag;
}
其中 ag_closed、ag_hooks_inited、ag_running_async 等字段正是 PEP 525 生命周期语义(aclose 钩子、GeneratorExit 传播、未关闭告警)在结构体中的落点;gen_finalize 会在解释器退出阶段调用 ag_origin_or_finalizer 挂的关闭钩子(Objects/genobject.c),对应 Python 层的 sys.set_asyncgen_hooks。
意外引入的 API:PyAsyncGenASend_CheckExact
文档最后单列了“Deprecated API”一节:
PyAsyncGenASend_CheckExact(op):This is an API that was included in Python's C API by mistake. It is solely here for completeness; do not use this API.(.. soft-deprecated:: 3.14)
它在头文件中就是一个类型检查宏(Include/cpython/genobject.h):
#define PyAsyncGenASend_CheckExact(op) Py_IS_TYPE((op), &_PyAsyncGenASend_Type)
_PyAsyncGenASend_Type 是异步生成器 asend(...) 返回的 awaitable 的内部类型。文档的定性很明确:这个检查宏是误入公共 C API 的,自 3.14 起被软废弃,仅为接口完整性而保留在文档中,扩展代码不应使用它——判断“是否为 asend awaitable”没有公开支持的路径。
适用前提与使用建议
把上述事实汇总成一张速查表(以当前仓库 3.16.0a0 为准):
| 接口 | 形态 | 状态 | 关键约定 |
|---|---|---|---|
PyGen_Type / PyAsyncGen_Type |
PyTypeObject 全局 |
有效 | 类型对象,供 isinstance 式检查与 MRO 定位 |
PyGen_Check |
宏(PyObject_TypeCheck) |
有效 | 含子类;入参不得为 NULL |
PyGen_CheckExact |
宏(Py_IS_TYPE) |
有效 | 精确类型;入参不得为 NULL |
PyGen_GetCode |
函数 | 有效 | 返回新强引用,总是成功 |
PyGen_New / PyGen_NewWithQualName |
函数,Py_DEPRECATED(3.16) |
3.16 废弃,3.18 移除 | 窃取 frame 引用(含出错路径);3.10 起已无法合法调用 |
PyAsyncGen_New |
函数,Py_DEPRECATED(3.16) |
3.16 废弃,3.18 移除 | 同上;成功返回强引用,失败 NULL + 异常 |
PyAsyncGen_CheckExact |
宏 | 有效 | 总是成功 |
PyAsyncGenASend_CheckExact |
宏 | 3.14 软废弃,勿用 | 误入 C API 的完整性条目 |
三条实践结论:
- 检查与检视可以安全使用:
PyGen_Check、PyGen_CheckExact、PyGen_GetCode、PyAsyncGen_CheckExact与两个PyTypeObject是当前扩展代码识别生成器、读取其代码对象的正规手段; - 构造生成器请走 Python 层:C 扩展要拿到生成器,应让 Python 调用方把生成器对象作为参数传入(或直接调用返回生成器的 Python 函数),而不是尝试 C 层构造——后者既被废弃,也在架构上不可行;
- 注意 API 边界:整套接口位于
Py_LIMITED_API之外,依赖稳定 ABI 的扩展无法直接引用这些符号;若未来面向 3.18 迁移,应同步删除对三个*_New构造函数的任何残留调用。
相关文档与源码入口:Doc/c-api/gen.rst(本文主体文档)、Include/cpython/genobject.h(public 声明)、Include/internal/pycore_interpframe_structs.h(结构布局)、Objects/genobject.c(实现,含 gen_new_with_qualname、PyAsyncGen_New 与类型对象定义)。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00