首页
/ CPython 协程对象 C API 解析:PyCoroObject、PyCoro_CheckExact 与 PyCoro_New 的弃用真相

CPython 协程对象 C API 解析:PyCoroObject、PyCoro_CheckExact 与 PyCoro_New 的弃用真相

2026-09-05 14:18:37作者:胡唯隽

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 async keyword 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);

注意两点事实:

  1. PyCoroObject 是一个不透明(opaque)类型——公开头文件只给出 typedef struct _PyCoroObject PyCoroObject;,完整布局位于内部头文件中。C 扩展不应假设其内存布局,只能把它当作 PyObject * 使用;
  2. 从源码结构看,协程对象与生成器、异步生成器共用一套底层骨架。PyCoro_Type 的定义见 Objects/genobject.c,其 tp_basicsize 计算方式为 offsetof(PyCoroObject, cr_iframe.localsplus)tp_itemsizesizeof(PyObject *)——即对象内部嵌入了一个解释器帧 _PyInterpreterFramecr_iframe),并以变长数组保存局部变量。这与生成器对象“调用即建帧”的模型一致。

PyCoro_Type 的类型名(tp_name)为 "coroutine",在 Python 层面就是 types.CoroutineType 所指的类型。其异步协议槽函数 coro_as_asyncObjects/genobject.c)注册了:

static PyAsyncMethods coro_as_async = {
    coro_await,                                 /* am_await */
    0,                                          /* am_aiter */
    0,                                          /* am_anext */
    PyGen_am_send,                              /* am_send  */
};

这解释了协程对象的两个关键协议能力:支持 awaitam_await 槽)以及支持 send()(复用生成器的 PyGen_am_send 实现)。类型定义中 tp_methodstp_memberstp_getset 分别指向 coro_methodscoro_memberlistcoro_getsetlist,源码中可见的方法包括 __sizeof____class_getitem__(后者让 coroutine[T] 泛型标注合法)。

二、PyCoro_CheckExact():唯一的类型检查入口

文档对该函数的描述是:

Return true if ob's type is PyCoro_Type; ob must not be NULL. 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.hPY_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;
}

可以看到,新协程直接从 PyFunctionObjectfunc->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()

三点结论:

  1. 基于 frame 的构造接口是成族弃用的,而非协程独有。PyGen_NewPyGen_NewWithQualNamePyCoro_NewPyAsyncGen_New 在同一头文件中统一被标注 Py_DEPRECATED(3.16)。C 扩展作者不应再把“手动构造协程/生成器”写进新代码;
  2. 该头文件整体位于 #ifndef Py_LIMITED_API 保护块内,属于非受限 C API,需要包含完整 Python 头文件才能使用;
  3. 当前仓库处于 3.16 开发线(3.16.0a0),因此 PyCoro_New() 在本仓库中仍然可用但会产生弃用告警;按文档的 deprecated-removed:: 3.16 3.18 标注,它将在 3.18 移除。编写面向未来的 C 扩展时应只依赖 PyCoro_TypePyCoro_CheckExact()

五、实操小结

结合 Doc/c-api/coro.rst 与当前仓库源码,C 扩展中与协程对象打交道的正确姿势是:

  • 识别协程:使用 PyCoro_CheckExact(obj);它展开为 Py_IS_TYPE((op), &PyCoro_Type),是精确类型判断,obj 不能为 NULL
  • 拿到类型对象:直接使用 PyCoro_Type 数据符号,可用于 PyObject_IsInstancePy_TYPE 比较、错误信息生成等场景;
  • 驱动协程执行:通过 Python 层协议(__await__send()throw()close())以 PyObject API 调用,而不是试图在 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_TypePyCoro_New_Py_MakeCoro_PyCoro_ComputeOrigin)来自 Objects/genobject.c,版本信息来自 Include/patchlevel.h

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

项目优选

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