CPython 协程对象 C API 解析:PyCoroObject、PyCoro_CheckExact 与 PyCoro_New 的弃用真相
CPython 官方文档中的 “Coroutine Objects” 页面(Doc/c-api/coro.rst)虽然篇幅不长,却定义了 C 扩展层面操作协程对象(coroutine object)的全部稳定接口:PyCoroObject 结构体、PyCoro_Type 类型对象、PyCoro_CheckExact() 类型检查,以及在 3.16 版本被正式标记弃用、3.18 将移除的 PyCoro_New() 构造函数。读完本文,你将掌握在 C 扩展中如何正确识别协程对象、理解协程对象在 CPython 内部的创建路径(_Py_MakeCoro),以及为什么 PyCoro_New() 这条“看似有用”的 API 实际上早已无法被正确使用。
一、协程对象是什么:定义与 C 结构
文档开篇给出了一句话定义:
Coroutine objects are what functions declared with an
asynckeyword return.协程对象是由
async关键字声明的函数被调用后返回的对象。
即 Python 层面的 async def 函数,每次调用并不会执行函数体,而是返回一个协程对象;真正驱动执行的是后续对 __await__ / send() 等协议方法的操作。该功能自 Python 3.5 引入(文档标注 versionadded:: 3.5)。
在 C API 层面,文档声明了两个核心符号:
/* C 结构体,用于协程对象(不透明类型) */
typedef struct _PyCoroObject PyCoroObject;
/* 与协程对象对应的类型对象 */
PyAPI_DATA(PyTypeObject) PyCoro_Type;
这两个声明在当前仓库中的实际位置是 Include/cpython/genobject.h:
/* --- PyCoroObject ------------------------------------------------------- */
typedef struct _PyCoroObject PyCoroObject;
PyAPI_DATA(PyTypeObject) PyCoro_Type;
#define PyCoro_CheckExact(op) Py_IS_TYPE((op), &PyCoro_Type)
Py_DEPRECATED(3.16) PyAPI_FUNC(PyObject *) PyCoro_New(PyFrameObject *,
PyObject *name, PyObject *qualname);
注意两点事实:
PyCoroObject是一个不透明(opaque)类型——公开头文件只给出typedef struct _PyCoroObject PyCoroObject;,完整布局位于内部头文件中。C 扩展不应假设其内存布局,只能把它当作PyObject *使用;- 从源码结构看,协程对象与生成器、异步生成器共用一套底层骨架。
PyCoro_Type的定义见 Objects/genobject.c,其tp_basicsize计算方式为offsetof(PyCoroObject, cr_iframe.localsplus),tp_itemsize为sizeof(PyObject *)——即对象内部嵌入了一个解释器帧_PyInterpreterFrame(cr_iframe),并以变长数组保存局部变量。这与生成器对象“调用即建帧”的模型一致。
PyCoro_Type 的类型名(tp_name)为 "coroutine",在 Python 层面就是 types.CoroutineType 所指的类型。其异步协议槽函数 coro_as_async(Objects/genobject.c)注册了:
static PyAsyncMethods coro_as_async = {
coro_await, /* am_await */
0, /* am_aiter */
0, /* am_anext */
PyGen_am_send, /* am_send */
};
这解释了协程对象的两个关键协议能力:支持 await(am_await 槽)以及支持 send()(复用生成器的 PyGen_am_send 实现)。类型定义中 tp_methods、tp_members、tp_getset 分别指向 coro_methods、coro_memberlist、coro_getsetlist,源码中可见的方法包括 __sizeof__ 与 __class_getitem__(后者让 coroutine[T] 泛型标注合法)。
二、PyCoro_CheckExact():唯一的类型检查入口
文档对该函数的描述是:
Return true if ob's type is
PyCoro_Type; ob must not beNULL. This function always succeeds.
在头文件中它被实现为一个宏(Include/cpython/genobject.h):
#define PyCoro_CheckExact(op) Py_IS_TYPE((op), &PyCoro_Type)
“Exact” 意味着精确类型检查:只接受 PyCoro_Type 类型的对象,不接受其子类实例。在 C 扩展中,这是判断“调用方传给我的到底是不是一个 async def 返回的协程”的标准手段。典型的防御式写法如下(示意代码,需配合 PyCoro_CheckExact 所在的非受限 C API 头文件使用):
static PyObject *
consume_coro(PyObject *self, PyObject *obj)
{
if (!PyCoro_CheckExact(obj)) {
PyErr_SetString(PyExc_TypeError,
"argument must be an exact coroutine object");
return NULL;
}
/* 通过 __await__ 协议驱动协程直至 StopAsyncIteration */
PyObject *await = PyObject_GetAttrString(obj, "__await__");
if (await == NULL) {
return NULL;
}
PyObject *iter = PyObject_CallNoArgs(await);
Py_DECREF(await);
if (iter == NULL) {
return NULL;
}
PyObject *ret = NULL;
for (;;) {
PyObject *next = PyIter_Next(iter);
if (next == NULL) {
if (PyErr_Occurred()) {
break; /* 真实错误 */
}
/* 迭代器耗尽:StopAsyncIteration,其 value 即协程返回值 */
/* (此处从异常对象中取 value 的完整实现从略) */
break;
}
Py_DECREF(next);
}
Py_DECREF(iter);
return ret;
}
这个示例同时印证了文档“Coroutine Objects”一节的隐含信息:驱动协程的正确协议是 __await__(对应上文的 am_await 槽)或 send(),而不是像普通迭代器那样直接对协程对象调用 next()。
三、PyCoro_New():一条正在消亡的构造函数
文档对 PyCoro_New() 的描述及其弃用标注,是整份文档信息量最大的部分:
.. c:function:: PyObject* PyCoro_New(PyFrameObject *frame, PyObject *name, PyObject *qualname)
Create and return a new coroutine object based on the *frame* object,
with ``__name__`` and ``__qualname__`` set to *name* and *qualname*.
A reference to *frame* is stolen by this function. The *frame* argument
must not be ``NULL``.
.. 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.
翻译要点:
- 功能:基于一个
frame对象创建协程对象,并设置__name__/__qualname__; - 所有权约定:函数会窃取对
frame的一个引用(stolen reference),调用方无需再释放它; - 弃用状态:在当前仓库版本(3.16 开发线,见 Include/patchlevel.h 中
PY_VERSION "3.16.0a0")标记deprecated,计划于 3.18 移除; - 弃用理由有两条:自 3.10 起 CPython 内部已不再调用它;且外部调用者根本无法构造出可供它使用的合法
frame对象。
第二条理由可以从源码结构直接印证。PyCoro_New() 的现行实现位于 Objects/genobject.c:
PyObject *
PyCoro_New(PyFrameObject *f, PyObject *name, PyObject *qualname)
{
PyObject *coro = gen_new_with_qualname(&PyCoro_Type, f, name, qualname);
if (!coro) {
return NULL;
}
PyThreadState *tstate = _PyThreadState_GET();
int origin_depth = tstate->coroutine_origin_tracking_depth;
if (origin_depth == 0) {
((PyCoroObject *)coro)->cr_origin_or_finalizer = NULL;
} else {
PyObject *cr_origin = _PyCoro_ComputeOrigin(origin_depth, _PyEval_GetFrame());
((PyCoroObject *)coro)->cr_origin_or_finalizer = cr_origin;
if (!cr_origin) {
Py_DECREF(coro);
return NULL;
}
}
return coro;
}
它依赖 gen_new_with_qualname() 将一个“尚未执行的 PyFrameObject”拷贝进协程对象(Objects/genobject.c)。问题在于:自 3.10 的帧对象重构(PEP 594 相关实现)之后,PyFrameObject 已成为只读的内部包装,真正的执行状态存放在不透明的 _PyInterpreterFrame 中;C 扩展无法再凭空“构造一个处于 CREATED 状态、owner 为 FRAME_OWNED_BY_FRAME_OBJECT 的帧”来喂给这个函数。这就是文档所说 “impossible to construct a proper frame object to call this function” 的技术根源。
那么 3.10 之后协程对象由谁创建?答案是内部的 _Py_MakeCoro()(Objects/genobject.c),它在 async def 函数被调用时由求值循环直接触发,按代码对象的标志位分派三种对象:
PyObject *
_Py_MakeCoro(PyFunctionObject *func)
{
int coro_flags = ((PyCodeObject *)func->func_code)->co_flags &
(CO_GENERATOR | CO_COROUTINE | CO_ASYNC_GENERATOR);
assert(coro_flags);
if (coro_flags == CO_GENERATOR) {
return make_gen(&PyGen_Type, func);
}
if (coro_flags == CO_ASYNC_GENERATOR) {
... /* PyAsyncGen_Type 分支 */
}
assert (coro_flags == CO_COROUTINE);
PyObject *coro = make_gen(&PyCoro_Type, func);
...
return coro;
}
可以看到,新协程直接从 PyFunctionObject(func->func_code)构造,__name__ 与 __qualname__ 取自 func->func_name / func->func_qualname(见 make_gen(),Objects/genobject.c),完全绕开了“外部传入 frame”这条死路。
PyCoro_New() 实现中还有一段值得注意的逻辑:创建协程时若线程状态里的 coroutine_origin_tracking_depth 大于 0,则调用 _PyCoro_ComputeOrigin()(Objects/genobject.c)收集调用栈上的若干帧(文件、行号、函数名)存入 cr_origin_or_finalizer 字段。这正是 asyncio 等库定位“协程在何处被创建却从未被 await”(RuntimeWarning: coroutine ... was never awaited)的数据来源。这也侧面说明:协程对象的“出生信息”必须在创建现场捕获,进一步削弱了事后用 PyCoro_New() 伪造对象的合理性。
四、同族 API 对照与版本约束
Include/cpython/genobject.h 将协程 API 与生成器、异步生成器 API 放在同一头文件中,三者的对照关系值得整理成表:
| 类型/接口 | 生成器 | 协程(本文主题) | 异步生成器 |
|---|---|---|---|
| 类型对象 | PyGen_Type |
PyCoro_Type |
PyAsyncGen_Type |
| 精确检查宏 | PyGen_CheckExact(op) |
PyCoro_CheckExact(op) |
PyAsyncGen_CheckExact(op) |
| 基于帧的构造函数 | PyGen_New / PyGen_NewWithQualName |
PyCoro_New |
PyAsyncGen_New |
| 3.16 弃用状态 | Py_DEPRECATED(3.16) |
Py_DEPRECATED(3.16) |
Py_DEPRECATED(3.16) |
| 3.10 后的实际创建路径 | make_gen(&PyGen_Type, ...) |
_Py_MakeCoro() / make_gen(&PyCoro_Type, ...) |
_Py_MakeCoro() |
三点结论:
- 基于
frame的构造接口是成族弃用的,而非协程独有。PyGen_New、PyGen_NewWithQualName、PyCoro_New、PyAsyncGen_New在同一头文件中统一被标注Py_DEPRECATED(3.16)。C 扩展作者不应再把“手动构造协程/生成器”写进新代码; - 该头文件整体位于
#ifndef Py_LIMITED_API保护块内,属于非受限 C API,需要包含完整 Python 头文件才能使用; - 当前仓库处于 3.16 开发线(
3.16.0a0),因此PyCoro_New()在本仓库中仍然可用但会产生弃用告警;按文档的deprecated-removed:: 3.16 3.18标注,它将在 3.18 移除。编写面向未来的 C 扩展时应只依赖PyCoro_Type与PyCoro_CheckExact()。
五、实操小结
结合 Doc/c-api/coro.rst 与当前仓库源码,C 扩展中与协程对象打交道的正确姿势是:
- 识别协程:使用
PyCoro_CheckExact(obj);它展开为Py_IS_TYPE((op), &PyCoro_Type),是精确类型判断,obj不能为NULL; - 拿到类型对象:直接使用
PyCoro_Type数据符号,可用于PyObject_IsInstance、Py_TYPE比较、错误信息生成等场景; - 驱动协程执行:通过 Python 层协议(
__await__、send()、throw()、close())以PyObjectAPI 调用,而不是试图在 C 层触碰PyCoroObject的内部字段; - 不要调用
PyCoro_New():自 3.10 起它已无内部调用者,且无法构造合法入参;在新代码中直接使用async def函数(调用函数本身即由_Py_MakeCoro()生成协程对象); - 关注创建位置追踪:若启用了
coroutine_origin_tracking_depth,每个协程都会携带创建栈信息(cr_origin),这是 asyncio 生态诊断“未被 await 的协程”的基础设施,其实现可对照 Objects/genobject.c 中的_Py_MakeCoro()与 Objects/genobject.c 中的_PyCoro_ComputeOrigin()。
本文的全部事实依据均来自当前仓库:API 语义与弃用状态来自 Doc/c-api/coro.rst,公开声明来自 Include/cpython/genobject.h,内部实现(PyCoro_Type、PyCoro_New、_Py_MakeCoro、_PyCoro_ComputeOrigin)来自 Objects/genobject.c,版本信息来自 Include/patchlevel.h。
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 StartedRust0623
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