首页
/ CPython 生成器对象 C API 全解:PyGenObject、类型检查、PyGen_GetCode 与 3.16 起废弃的构造接口

CPython 生成器对象 C API 全解:PyGenObject、类型检查、PyGen_GetCode 与 3.16 起废弃的构造接口

2026-09-04 14:02:26作者:冯爽妲Honey

CPython 用 Generator Objects 实现生成器迭代器,这是 C 扩展与解释器内部交互的核心对象之一。本篇基于 CPython 官方文档 Doc/c-api/gen.rst,结合 Include/cpython/genobject.hObjects/genobject.c 的源码实现,完整讲清生成器对象的 C 接口面:类型对象、PyGen_Check / PyGen_CheckExact 检查宏、PyGen_GetCode 强引用语义、PyGenObject 内部结构布局,以及 PyGen_NewPyGen_NewWithQualNamePyAsyncGen_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_TypePyCoro_New)与异步生成器(PyAsyncGen_TypePyAsyncGen_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_CheckPyObject_TypeCheck,沿 MRO 查找,因此 types.GeneratorType 的子类实例也会返回真;
  • PyGen_CheckExactPy_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;
}

可以看到:

  1. 内部辅助函数 _PyGen_GetCode 返回借用引用,直接从内嵌帧 gen->gi_iframe 读取代码对象;
  2. 公共 API PyGen_GetCode 在其上做 Py_INCREF,兑现“新强引用”的承诺;
  3. 入口处 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_GetGeneratorFromFrameInclude/internal/pycore_genobject.h)甚至能用 offsetof(PyGenObject, gi_iframe) 从帧指针反推出生成器对象,说明帧被刻意放在对象头部以支持这种指针运算;
  • gi_frame_state:记录帧当前所处阶段(创建、运行、挂起于 yield / yield from 等)。在无 GIL 构建(Py_GIL_DISABLED)下,状态迁移会用原子 CAS 完成,参见 Objects/genobject.cgen_try_set_frame_state
  • 同一段 _PyGenObject_HEAD 宏还被 PyCoroObjectcr_ 前缀)与 PyAsyncGenObjectag_ 前缀)复用,这正是三类对象共享大部分方法表与遍历/清理逻辑的根源。

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__

两者都要求 frameNULL,且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_qualnameObjects/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);
}

实现要点与文档语义一一对应:

  1. 可变大小 GC 对象:用 PyObject_GC_NewVarco_nlocalsplus + co_stacksize 分配,本地变量与操作数栈直接追加在生成器结构体尾部,避免额外堆分配;
  2. 帧拷贝而非帧转移_PyFrame_CopyPyFrameObject 的帧数据拷入 gen->gi_iframe,随后将帧 owner 置为 FRAME_OWNED_BY_GENERATOR 并设置 gi_frame_state = FRAME_CREATED
  3. name/qualname 回退规则:不显式传入时取代码对象的 co_name / co_qualname——这就是 Python 层 gen.__name__ 的默认来源;
  4. 引用窃取语义:分配失败路径上先 Py_DECREF(f) 再返回 NULL,成功路径在拷贝完成后 Py_DECREF(f),无论成败,f 的引用都不再属于调用者,与文档的 “stolen (even on error)” 完全一致。

对扩展作者的实践含义:不要在自己代码中调用这三个构造函数PyGen_NewPyGen_NewWithQualName、同族被废弃的 PyCoro_New),编译器也会用 Py_DEPRECATED(3.16) 给出警告;3.18 起它们将彻底消失,届时编译失败会成为硬性约束。

异步生成器对象(PEP 525)

文档后半部分对应 PEP 525 定义的异步生成器(async defasync 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_closedag_hooks_initedag_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 的完整性条目

三条实践结论:

  1. 检查与检视可以安全使用PyGen_CheckPyGen_CheckExactPyGen_GetCodePyAsyncGen_CheckExact 与两个 PyTypeObject 是当前扩展代码识别生成器、读取其代码对象的正规手段;
  2. 构造生成器请走 Python 层:C 扩展要拿到生成器,应让 Python 调用方把生成器对象作为参数传入(或直接调用返回生成器的 Python 函数),而不是尝试 C 层构造——后者既被废弃,也在架构上不可行;
  3. 注意 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_qualnamePyAsyncGen_New 与类型对象定义)。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
982
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384