首页
/ CPython C API 全景指南:头文件组织、引用计数、异常处理与嵌入 Python 的实战详解

CPython C API 全景指南:头文件组织、引用计数、异常处理与嵌入 Python 的实战详解

2026-09-04 15:09:28作者:农烁颖Land

本文基于 CPython 官方文档 Doc/c-api/intro.rst 系统梳理 Python/C API 的入门知识体系:从 Python.h 头文件的包含规则与命名空间约定,到 PY_SSIZE_T_CLEAN 等关键宏的正确用法;从 PyObject* 的对象模型、引用计数的“新引用/借用/窃取”所有权语义,到 C 层面显式异常检查的 goto 错误处理范式;再到 Py_Initialize 的嵌入式集成与调试构建选项。读完本文,你既能直接上手编写 C/C++ 扩展模块,也能在更大的 C/C++ 应用中正确嵌入并驱动 CPython 解释器,并且能对照源码确认每个 API 行为背后的真实实现。

一、API 的两大用途:扩展模块与嵌入 Python

CPython 的 C API 面向 C 与 C++ 程序员,在多个层次上开放了解释器能力。API 从 C++ 同样可用,但由于历史习惯,一般统称为 Python/C API。使用它的动机有两类,且性质截然不同:

  1. 编写扩展模块(extension modules):用 C 代码扩展解释器能力,这是最常见的用途。文档将其描述为“相对成熟、可按菜谱(cookbook)操作”的流程,且有多款自动化工具降低门槛。
  2. 嵌入 Python(embedding):把 Python 作为更大 C/C++ 应用的一个组件来使用。这个过程比写扩展“不那么 straightforward”,从 Python 诞生早期就有人在这样做。

文档给出了一条重要的学习路径建议:许多 API 函数在“扩展”和“嵌入”两种场景下都有用,而且几乎所有嵌入 Python 的应用都还需要提供自定义扩展模块——因此在真正动手嵌入之前,先熟悉扩展模块的写法是更稳妥的路线。

二、语言版本兼容性与编码规范

  • 语言标准底线:Python 的 C API 兼容 C11 与 C++11 及以上的版本。这只是一个下限——API 不依赖更晚标准的新特性,你也不需要在编译器中显式开启 “c11 mode”。
  • 编码规范:如果你写的 C 代码要合入 CPython 本身,必须遵循 PEP 7 定义的规范(该文档以 :PEP:7`` 形式引用);对于自己的第三方扩展模块,除非打算将来向 CPython 贡献,否则不强制遵循。

三、Include 头文件:Python.h 是唯一入口

3.1 标准包含方式

所有使用 C API 的代码都应该这样引入头文件:

#define PY_SSIZE_T_CLEAN
#include <Python.h>

两条规则必须遵守:

  • Python.h 必须先于任何标准头文件包含。因为 Python 可能在某些系统上定义影响标准头文件行为的预处理宏(例如 ssize_t 相关的定义)。
  • 建议始终在包含 Python.h 之前定义 PY_SSIZE_T_CLEAN。该宏影响 PyArg_ParseTuple 等参数解析函数中 # 格式字符对 Py_ssize_t 的处理(文档指向 arg-parsing 章节说明细节)。

从源码可以印证其“meta-include”性质:Include/Python.h 文件头注释明确写着 “Since this is a 'meta-include' file, #ifdef __cplusplus / extern "C" {”——它正是通过 extern "C" 守卫来保证 C++ 用户无需任何特殊处理即可调用纯 C 定义的 API。该文件依次包含了 <assert.h><inttypes.h><limits.h><math.h><stdarg.h><string.h><wchar.h><sys/types.h>(若存在)等头文件,并在条件编译下包含 <ctype.h><unistd.h>(POSIX)、<errno.h><stdio.h><stdlib.h>(后几项属于向后兼容包含)。

3.2 命名空间约定

Python.h 定义的所有用户可见名字(标准头文件除外)都带 Py_Py 前缀:

  • Py 前缀:对外公开 API;
  • _Py 前缀:解释器内部使用,扩展模块作者不应使用。

结构体成员名则没有保留前缀。文档同时警告:用户代码永远不要自己定义以 Py_Py 开头的名字——既会让读者困惑,也会损害代码在未来 Python 版本下的可移植性(未来版本可能新增同名符号导致冲突)。

3.3 头文件安装位置与多平台搜索路径

  • Unix:头文件位于 {prefix}/include/pythonX.Y/{exec_prefix}/include/pythonX.Y/ 两个目录,其中 prefix/exec_prefixconfigure 脚本的对应参数决定,X.Y'%d.%d' % sys.version_info[:2]
  • Windows:头文件安装在 {prefix}/includeprefix 即安装器指定的安装目录。

关键陷阱:编译时应把这两个目录(若不同)直接放入编译器头文件搜索路径,而不是把父目录放进搜索路径后写 #include <pythonX.Y/Python.h>——因为多平台构建下,prefix 下的平台无关头文件会反过来包含 exec_prefix 下的平台相关头文件,这种反向嵌套会破坏构建。

3.4 系统包含(System includes)

文档明确列出了 Python.h隐式包含清单,并提醒:C 扩展应显式包含自己用到的标准头文件,不要依赖这些隐式包含

当前隐式包含:

  • <assert.h><intrin.h>(Windows)、<inttypes.h><limits.h><math.h><stdarg.h><string.h><wchar.h><sys/types.h>(若存在)

仅在非 Limited API 3.13+ 时为向后兼容而包含:<ctype.h><unistd.h>(POSIX)。 仅在非 Limited API 3.11+ 时为向后兼容而包含:<errno.h><stdio.h><stdlib.h>

这与 Include/Python.h 中的实际 #include 列表一致,也提示了 Limited API(稳定 ABI)路线的方向:新版 Limited API 会逐渐停止这些兼容包含,第三方扩展应自行管理标准头文件。

四、常用宏(Useful macros)

很多宏定义在“最接近其用途”的地方(如 Py_RETURN_NONEPyMODINIT_FUNC),本节文档集中列出的是一般用途宏。

4.1 系统能力与环境变量

  • Py_CAN_START_THREADS(3.13 新增):若已定义,表示当前系统可以启动线程。按照 PEP 11 的支持平台范围,目前除部分 WebAssembly 平台外全部支持。
  • Py_GETENV(s):类似 getenv(s),但当解释器以 -E 选项启动时(等价于 PyConfig.use_environment 关闭)返回 NULL——扩展模块用它取环境变量才能尊重隔离语义。

4.2 文档字符串宏(Docstring macros)

这三个宏解决“无 docstrings 构建”(--without-doc-strings)下的兼容问题,源码位于 Include/pymacro.h

#define PyDoc_VAR(name)   static const char name[]
// 有 docstrings 时:
#define PyDoc_STR(str)    str
// 无 docstrings 时:
#define PyDoc_STR(str)    ""
#define PyDoc_STRVAR(name, str)  PyDoc_VAR(name) = PyDoc_STR(str)

典型用法:

PyDoc_STRVAR(pop_doc, "Remove and return the rightmost element.");

static PyMethodDef deque_methods[] = {
    // ...
    {"pop", (PyCFunction)deque_pop, METH_NOARGS, pop_doc},
    // ...
};

也可以直接内联在方法表里:

static PyMethodDef pysqlite_row_methods[] = {
    {"keys", (PyCFunction)pysqlite_row_keys, METH_NOARGS,
        PyDoc_STR("Returns the keys of the row.")},
    {NULL, NULL}
};

4.3 通用工具宏

  • Py_UNUSED(arg)(3.4 新增):用于消静音调用不到的函数参数告警,例如 int func(int a, int Py_UNUSED(b)) { return a; }。在 Include/pymacro.h 中,GCC 家族编译器展开为 _unused_##name __attribute__((unused)),MSVC 使用 __pragma 抑制 C4100 告警。
  • Py_GCC_ATTRIBUTE(name):在支持 GCC 属性的编译器上展开为 __attribute__((name)),在 MSVC 等编译器上展开为空。Include/pyport.h 中可以看到其双分支定义。

4.4 数值工具

  • Py_ABS(x)(3.3 新增):求绝对值,近似 ((x) < 0 ? -(x) : (x))。参数可能被求值多次,不要传带副作用的表达式;若结果不可表示(如 int 类型的 INT_MIN 取反),行为未定义。
  • Py_MAX(x, y) / Py_MIN(x, y)(3.3 新增):返回较大/较小值,同样允许多次求值,避免副作用参数。Include/pymacro.h 中的实际定义即三元表达式 (((x) > (y)) ? (y) : (x))
  • Py_ARITHMETIC_RIGHT_SHIFT(type, integer, positions)(3.1 变更):类似 integer >> positions,但强制符号扩展——因为 C 标准并未规定有符号整数右移是算术右移还是逻辑右移。type 参数已废弃仅为向后兼容保留。3.1 起对所有有符号整数类型有效。
  • Py_CHARMASK(c):参数必须是字符或 [-128, 127](或 [0, 255])范围内的整数,返回其 unsigned char 转换结果。

4.5 断言与类型尺寸工具

  • Py_UNREACHABLE()(3.7 新增):用于“设计上不可能到达”的代码路径,典型场景是 switchdefault: 分支(所有取值已被 case 覆盖)。发布模式下它帮助编译器优化并消除不可达告警——GCC 上实现为 __builtin_unreachable(),MSVC 上为 __assume(0),这一点可在 Include/pymacro.h 中逐分支看到;调试模式与不支持的编译器上则展开为 Py_FatalError 调用。注意:对“很罕见但确实可能到达”的路径(低内存、系统调用返回意外值)绝不能使用它,应把错误上报给调用者或改用 Py_FatalError

  • Py_SAFE_DOWNCAST(value, larger, smaller):把 valuelarger 转为 smaller 并验证无信息丢失。发布构建近似 ((smaller) value)(C++ 下用 static_cast);调试构建(定义 Py_DEBUG)会断言转换无截断。参数可能多次求值,勿传副作用表达式。

  • Py_BUILD_ASSERT(cond)(3.3 新增):编译期条件断言(语句形式),条件为假或无法在编译期求值时构建失败,C23 下近似 static_assert(cond)。例如:

    Py_BUILD_ASSERT(sizeof(PyTime_t) == sizeof(int64_t));
    
  • Py_BUILD_ASSERT_EXPR(cond)(3.3 新增):同样的编译期断言,但以“值为 0 的表达式”形式给出,可用于宏定义内部:

    #define foo_to_char(foo) \
        ((char *)(foo) + Py_BUILD_ASSERT_EXPR(offsetof(struct foo, string) == 0))
    
  • Py_ARRAY_LENGTH(array):编译期计算静态数组长度,近似 sizeof(array) / sizeof((array)[0])必须传入编译期可知大小的 C 数组;堆分配数组在某些编译器上会编译错误,否则产生错误结果。

  • Py_MEMBER_SIZE(type, member)(3.6 新增):返回结构体成员的字节大小,近似 sizeof(((type *)NULL)->member)Include/pymacro.h 中实际写法是 sizeof(((type *)0)->member)

4.6 宏定义工具

  • Py_FORCE_EXPANSION(X):等价于 X,但对宏展开“强制求值”,在 token-pasting(## 粘连)场景中确保参数先展开。
  • Py_STRINGIFY(x)(3.4 新增):把 x 转成 C 字符串字面量,如 Py_STRINGIFY(123) 得到 "123"

4.7 声明工具(主要用于 CPython 自身定义 C API)

这些宏大多展开为各编译器的通用扩展写法,扩展模块作者使用较少:

作用
Py_ALWAYS_INLINE(3.11 新增) 请求编译器总是内联 static inline 函数;GCC 对应 always_inline,MSVC 对应 __forceinline。在 Py_DEBUG 调试构建下不产生任何效果。必须写在返回类型之前,如 static inline Py_ALWAYS_INLINE int random(void) { return 4; }。文档提醒:盲目标注会导致代码膨胀、性能反而变差,编译器通常比开发者更懂成本收益分析。
Py_NO_INLINE(3.11 新增) 禁止内联,对应 GCC/MSVC 的 noinline,例如可降低 LTO+PGO 重度内联构建的 C 栈消耗。用法 Py_NO_INLINE static int random(void) { return 4; }
Py_DEPRECATED(version) 声明在指定 CPython 版本废弃的 API,放在符号名之前,如 Py_DEPRECATED(3.8) PyAPI_FUNC(int) Py_OldFunction(void);
Py_LOCAL(type) / Py_LOCAL_INLINE(type) 以快速调用限定符声明文件内局部函数,语义等价 static type;后者额外请求内联。
Py_LOCAL_SYMBOL 声明符号对共享库局部(hidden);GCC/Clang 上展开为 __attribute__((visibility("hidden")))
Py_EXPORTED_SYMBOL 声明导出符号:Windows 上展开为 __declspec(dllexport),GCC/Clang 上为 visibility("default")仅供定义 C API 本身使用,扩展模块不得用。
Py_IMPORTED_SYMBOL 声明导入符号,Windows 上为 __declspec(dllimport)。同样仅限 CPython 内部。
PyAPI_FUNC(type) / PyAPI_DATA(type) CPython 声明 C API 函数/公开全局变量的官方宏,展开取决于平台与构建配置;扩展模块不应把它们用于自己的符号。

4.8 过时宏(Outdated macros)

以下宏因对应能力已被 C11(或更早标准)标准化而软废弃,新代码应使用标准设施:

  • Py_ALIGNED(num)(3.15 软废弃):改用标准 alignas
  • PY_FORMAT_SIZE_T(3.15 软废弃):printfsize_t 修饰符,直接用 "z"
  • Py_LL(number) / Py_ULL(number)(3.15 软废弃):C99 起直接写 LL / LLU 后缀即可。
  • PY_LONG_LONGPY_INT32_TPY_UINT32_TPY_INT64_TPY_UINT64_T(3.15 软废弃):分别是 long longint32_t 等标准类型的别名,直接使用标准类型。
  • PY_LLONG_MINPY_LLONG_MAXPY_ULLONG_MAXPY_SIZE_MAX(3.15 软废弃):标准名 LLONG_MIN 等(<limits.h> 已由 Python.h 包含)。
  • Py_MEMCPY(dest, src, n)(3.14 起建议直接用 memcpy):memcpy 的别名。
  • Py_UNICODE_SIZE(3.15 软废弃):改用 sizeof(wchar_t)WCHAR_WIDTH/8
  • Py_UNICODE_WIDE(3.15 软废弃):改用 sizeof(wchar_t) >= 4 判断。
  • Py_VA_COPY(3.6 起已是 C99 标准 va_copy 的别名,3.15 软废弃):直接用 va_copy

五、对象、类型与引用计数

5.1 PyObject*:统一的对象指针

C API 的大部分函数以 PyObject* 作为参数与返回值。它是指向不透明数据类型的指针,表示任意 Python 对象——所有 Python 对象类型在赋值、作用域、传参等场景中都被语言同等对待,用单一 C 类型表示是顺理成章的。

  • 几乎所有 Python 对象都活在堆上:你只能声明 PyObject* 指针变量,不能声明 PyObject 类型的自动/静态变量。唯一例外是类型对象:它们绝不能被释放,通常以静态的 PyTypeObject 形式存在。
  • 所有对象(连 Python 整数在内)都有类型引用计数。每个知名类型都有检查宏,如 PyList_Check(a) 当且仅当 *a 是 Python 列表时为真。

5.2 引用计数语义

引用计数统计的是指向对象的**强引用(strong reference)**数量——来源可以是其他对象、全局(静态)C 变量,或某个 C 函数的局部变量。最后一个强引用释放(计数归零)时对象被释放;若它持有对其他对象的引用,这些引用随之释放,并可能级联触发进一步释放。(对象互相引用会形成环,文档的当下建议是“别那么做”。)

引用计数永远显式操作Py_INCREF 取新引用(+1),Py_DECREF 释放引用(-1)。Py_DECREF 比 incref 复杂得多:它必须检查计数是否归零,归零则调用对象类型结构中的deallocator 函数指针——类型特有的 dealloc 负责释放复合对象内部引用并执行收尾工作。引用计数不会溢出:持有它的位数至少与虚拟地址空间中的可区分内存位置数一样多(假设 sizeof(Py_ssize_t) >= sizeof(void*))。

从源码层面看,Include/refcount.hPy_DECREF 有多个按构建配置选择的实现,最能说明文档所述“检查归零并触发 dealloc”的机制——最普通的非受限 API 路径为:

static inline Py_ALWAYS_INLINE void Py_DECREF(PyObject *op)
{
    if (_Py_IsImmortal(op)) {
        return;
    }
    if (--op->ob_refcnt == 0) {
        _Py_Dealloc(op);
    }
}

此外该文件还展示了三种变体:受限 API 3.12+ / 调试构建下改走 Py_DecRef() 函数调用(便于稳定 ABI);Py_REF_DEBUG 下附加“负引用计数”检查与文件行号记录;Py_GIL_DISABLED(无 GIL 构建)下引用计数拆分为线程局部的 ob_ref_local 与共享的 ob_ref_shared 两级字段,local 归零时调用 _Py_MergeZeroLocalRefcount 合并计数。这些都印证了文档强调的一点:Py_DECREF 背后可能执行任意代码路径(deallocator 会递归清理并可能回到 Python 层)

5.3 何时不必临时 incref

文档澄清了一个常见误区:并非每个持有对象指针的局部变量都需要先 Py_INCREF。理论上变量指向对象时 +1、出作用域时 -1,两者互相抵消,计数不变;引用计数存在的唯一真实目的,是在变量指向对象的期间阻止对象被释放。如果你确信还有另一个活得不比你这个变量短的引用存在(例如扩展模块的 C 函数从 Python 被调用时,调用机制在整个调用期间保证持有所有实参的引用),就无需临时取新引用。

经典陷阱:从列表中取出一个对象后长时间持有,却未取新引用——其他操作可能把它移出列表、释放该引用甚至释放对象本身。危险在于,许多“看起来人畜无害”的操作都可能执行任意 Python 代码,Py_DECREF 本身就有把控制权交还用户的代码路径,因此几乎任何操作都潜在危险。

安全准则:始终优先使用通用(generic)操作,即函数名以 PyObject_PyNumber_PySequence_PyMapping_ 开头的函数——它们总是返回新强引用,调用者随后负责 Py_DECREF

5.4 引用的所有权:new / borrowed / stolen

文档用“引用所有权(ownership of references)”来精确刻画 API 行为。所有权永远属于引用而非对象(对象只被共享、不被拥有):

  • 拥有引用 = 有责任在不再需要时调用 Py_DECREF
  • 所有权可以转移:接收方成为新责任人,可以自行释放或继续上抛(通常抛给调用者)。函数把所有权交给调用者时,调用者得到一个新引用(new reference)
  • 不发生转移时,调用者只是借用(borrow) 该引用,对借用引用什么都不用做;
  • 反方向:调用函数时传入的引用要么被函数窃取(steal),要么不被窃取。被窃取意味着函数成为该引用的新主人,它可以随时 Py_DECREF,因此调用方在调用后不得再使用该引用

很少函数会窃取引用,著名例外只有两个:PyList_SetItemPyTuple_SetItem(窃取 item 的引用,但不窃取放入其中的 tuple/list 本身的引用!)。它们之所以设计为窃取,是为了配合“用新建对象填充 tuple/list”这一高频惯用法:

PyObject *t;

t = PyTuple_New(3);
PyTuple_SetItem(t, 0, PyLong_FromLong(1L));
PyTuple_SetItem(t, 1, PyLong_FromLong(2L));
PyTuple_SetItem(t, 2, PyUnicode_FromString("three"));

此处 PyLong_FromLong 返回的新引用立刻被 PyTuple_SetItem 窃取。若想在引用被窃取后继续使用对象,先 Py_INCREF 再调用。

两个重要细节:

  • PyTuple_SetItem唯一能设置 tuple 元素的方式:PySequence_SetItemPyObject_SetItem 会拒绝操作(tuple 不可变)。且只应使用在你自己正在创建的 tuple 上。
  • 实际上你很少手写上面这种填充代码——通用函数 Py_BuildValue 可以用格式字符串从 C 值创建绝大多数常见对象,并顺带处理错误检查:
PyObject *tuple, *list;

tuple = Py_BuildValue("(iis)", 1, 2, "three");
list  = Py_BuildValue("[iis]", 1, 2, "three");

而更常见的是:使用 PyObject_SetItem 等函数时,item 往往是只借用的引用(比如传进你函数的实参)。此时引用行为更健康——你不必为了“把引用送出去”而先取新引用。文档给出的完整示例:把可变序列的所有元素设为给定 item:

int
set_all(PyObject *target, PyObject *item)
{
    Py_ssize_t i, n;

    n = PyObject_Length(target);
    if (n < 0)
        return -1;
    for (i = 0; i < n; i++) {
        PyObject *index = PyLong_FromSsize_t(i);
        if (!index)
            return -1;
        if (PyObject_SetItem(target, index, item) < 0) {
            Py_DECREF(index);
            return -1;
        }
        Py_DECREF(index);
    }
    return 0;
}

5.5 返回值的所有权取决于函数,而不是对象类型

函数返回的对象引用,很多函数会把所有权给你——因为返回对象常常是临时创建的,你拿到的就是唯一引用。因此 PyObject_GetItemPySequence_GetItem 等通用函数总是返回新引用。文档特别强调一个反直觉点:你是否拥有返回引用,只取决于调用哪个函数,与实参对象的“羽毛”(类型)毫无关系——PyList_GetItem 取出列表元素你不拥有引用,而 PySequence_GetItem(参数签名几乎相同)取出同一元素,你却拥有新引用。

文档用一对“求整型列表和”的对照函数演示差异。借用引用的版本(PyList_GetItem,不能失败):

long
sum_list(PyObject *list)
{
    Py_ssize_t i, n;
    long total = 0, value;
    PyObject *item;

    n = PyList_Size(list);
    if (n < 0)
        return -1; /* Not a list */
    for (i = 0; i < n; i++) {
        item = PyList_GetItem(list, i); /* Can't fail */
        if (!PyLong_Check(item)) continue; /* Skip non-integers */
        value = PyLong_AsLong(item);
        if (value == -1 && PyErr_Occurred())
            /* Integer too big to fit in a C long, bail out */
            return -1;
        total += value;
    }
    return total;
}

拥有引用的版本(PySequence_GetItem,每条路径都要 Py_DECREF):

long
sum_sequence(PyObject *sequence)
{
    Py_ssize_t i, n;
    long total = 0, value;
    PyObject *item;
    n = PySequence_Length(sequence);
    if (n < 0)
        return -1; /* Has no length */
    for (i = 0; i < n; i++) {
        item = PySequence_GetItem(sequence, i);
        if (item == NULL)
            return -1; /* Not a sequence, or other failure */
        if (PyLong_Check(item)) {
            value = PyLong_AsLong(item);
            Py_DECREF(item);
            if (value == -1 && PyErr_Occurred())
                /* Integer too big to fit in a C long, bail out */
                return -1;
            total += value;
        }
        else {
            Py_DECREF(item); /* Discard reference ownership */
        }
    }
    return total;
}

5.6 Py_ssize_t

除基本 C 类型(intlongdoublechar*)外,少数结构体类型用于描述模块导出函数表、新类型的数据属性表等静态表,另有复杂数值的值类型——这些随使用它们的函数一起讨论。最重要的独立类型是:

  • Py_ssize_t:有符号整数类型,满足 sizeof(Py_ssize_t) == sizeof(size_t)(C99 并未直接定义这样的有符号类型,详见 PEP 353)。PY_SSIZE_T_MAX 是其最大正值。

源码印证:Include/pyport.h 在 Linux/POSIX 下直接 typedef ssize_t Py_ssize_t,否则回退到 Py_intptr_t,两者皆不可用时以 #error 终止编译;同文件中还可见 Py_ssize_clean_t 等与 PY_SSIZE_T_CLEAN 配套的辅助类型。

六、C 层面的异常处理

Python 程序员只有在需要特定错误处理时才接触异常:未处理的异常自动向上传播直至顶层解释器,并伴随 traceback 报告给用户。但对 C 程序员,错误检查必须永远显式

  • C API 的每个函数都可能抛出异常,除非其文档明确声明否则;
  • 出错时函数的惯用行为:设置异常 → 丢弃自己拥有的对象引用 → 返回错误指示符。除非另有文档说明,指示符是 NULL-1(取决于返回类型);少数函数返回布尔值(false 表示错误);极少数函数没有显式指示符或返回值二义,需显式调用 PyErr_Occurred 检查——这些都会被明确文档化。
  • 异常状态存于线程局部存储(单线程程序下等价于全局存储),线程处于“有异常”或“无异常”两态之一。PyErr_Occurred 用于检查:有异常时返回异常类型对象的借用引用,否则返回 NULLPyErr_SetString 是最常用的设置异常函数(但不是最通用的),PyErr_Clear 清除异常状态。

完整的异常状态由三个对象组成(都可为 NULL):异常类型、异常值、traceback——与 sys.exc_info() 的三元组含义相同,但不是同一份数据:Python 层对象代表正在被 try...except 处理的异常;C 层异常状态只在异常穿过 C 函数链期间存在,直到字节码解释器主循环接手,转交给 sys.exc_info() 等。

文档还梳理了历史语义(自 Python 1.5 起):

  • 线程安全的访问方式是 sys.exc_info()
  • 捕获异常的函数会保存并恢复其线程的异常状态,以免“人畜无害的函数覆盖正在处理的异常”这一常见 bug,同时减少 traceback 栈帧引用造成的对象生命周期延长。

黄金原则:调用了另一个函数完成任务后,应检查它是否抛异常;若是,把异常状态原样上抛,丢弃自己拥有的引用并返回错误指示符——绝不能设置另一个异常,否则会覆盖原始异常、丢失错误根因信息。

6.1 完整示例:incr_item(含 goto 错误清理)

先给出 Python 版以便对照:

def incr_item(dict, key):
    try:
        item = dict[key]
    except KeyError:
        item = 0
    dict[key] = item + 1

对应的 C 实现(文档原文称之为“它的全部荣光”):

int
incr_item(PyObject *dict, PyObject *key)
{
    /* Objects all initialized to NULL for Py_XDECREF */
    PyObject *item = NULL, *const_one = NULL, *incremented_item = NULL;
    int rv = -1; /* Return value initialized to -1 (failure) */

    item = PyObject_GetItem(dict, key);
    if (item == NULL) {
        /* Handle KeyError only: */
        if (!PyErr_ExceptionMatches(PyExc_KeyError))
            goto error;

        /* Clear the error and use zero: */
        PyErr_Clear();
        item = PyLong_FromLong(0L);
        if (item == NULL)
            goto error;
    }
    const_one = PyLong_FromLong(1L);
    if (const_one == NULL)
        goto error;

    incremented_item = PyNumber_Add(item, const_one);
    if (incremented_item == NULL)
        goto error;

    if (PyObject_SetItem(dict, key, incremented_item) < 0)
        goto error;
    rv = 0; /* Success */
    /* Continue with cleanup code */

error:
    /* Cleanup code, shared by success and failure path */

    /* Use Py_XDECREF() to ignore NULL references */
    Py_XDECREF(item);
    Py_XDECREF(const_one);
    Py_XDECREF(incremented_item);

    return rv; /* -1 for error, 0 for success */
}

这段代码示范了 C API 编程的几条“官方背书”惯例:

  1. goto 用于错误清理是被认可的正确用法——成功与失败路径共享同一段清理代码;
  2. PyErr_ExceptionMatches + PyErr_Clear:精确处理特定异常(这里只捕获 KeyError),其余异常直接上抛;
  3. Py_XDECREF(名字里的 X 即“可空”):释放可能为 NULL 的拥有引用——若用 Py_DECREF 遇到 NULL 会崩溃;因此所有持有拥有引用的变量都初始化为 NULL
  4. 返回值先初始化为失败值(-1),只有最后一次调用成功后才置为 0。

七、嵌入 Python(Embedding)

嵌入者(相对扩展作者而言)独有的核心任务只有一件:解释器的初始化(以及可能的终结)——解释器的大多数功能只有在初始化之后才能使用。

7.1 基本初始化

  • Py_Initialize:初始化已加载模块表,创建三个基本模块 builtins__main__sys,并初始化模块搜索路径(sys.path)。
  • 注意Py_Initialize 不会设置 sys.argv。若后续执行的 Python 代码需要它,必须设置 PyConfig.argvPyConfig.parse_argv(参见 init-config 章节的 Python Initialization Configuration)。
  • sys.path 的推导逻辑:在多数系统上(Unix 与 Windows 细节略有差异),解释器基于“对标准解释器可执行文件位置的最佳猜测”来计算搜索路径,假设 Python 库位于可执行文件的固定相对位置。具体做法是在 PATH 环境变量上查找名为 python 的可执行文件,并以其父目录为基准寻找 lib/python{X.Y} 目录。例如找到 /usr/local/bin/python 就假设库在 /usr/local/lib/python{X.Y}——该路径同时是兜底位置PATH 上找不到 python 可执行文件时使用)。用户可用环境变量 PYTHONHOME 覆盖此行为,或用 PYTHONPATH 在标准路径之前插入额外目录。
  • 程序名引导搜索:嵌入应用可以在调用 Py_InitializeFromConfig 之前设置 PyConfig.program_name 来引导搜索;但 PYTHONHOME 仍会覆盖该设置,PYTHONPATH 仍会前置插入。
  • 仓库中的 Programs/python.c 即官方解释器入口,展示了从 main 到解释器初始化的真实链路,可作为嵌入程序的参照样板。

7.2 终结与状态查询

  • Py_FinalizeEx:用于“反初始化”——应用想重新开始(再次 Py_Initialize)或用完 Python 想释放其分配的内存时调用。注意:它并不释放解释器分配的全部内存,例如扩展模块分配的当前无法回收。
  • Py_IsInitialized:返回 Python 当前是否处于已初始化状态。

八、调试构建(Debugging Builds)

Python 可以用多个宏构建以启用对解释器与扩展模块的额外检查;这些检查带来大量运行时开销,因此默认关闭。完整的构建类型清单见源码分发包中的 Misc/SpecialBuilds.txt——其中提供了引用计数跟踪、内存分配器调试、主解释器循环低层剖析等构建。最常用的两个:

  • Py_DEBUG:定义该宏构建解释器即得到通常所说的“Python 调试构建”。
    • Unix:在 ./configure 命令加 --with-pydebug(同时关闭编译器优化);
    • Windows:给 PCbuild/build.bat-d 自动启用;此外编译器定义的 _DEBUG 宏也会隐式启用 Py_DEBUG
    • 除下文引用计数调试外还有额外检查。
  • Py_TRACE_REFS:启用引用跟踪(configure 选项 --with-trace-refs)。定义后,每个 PyObject 增加两个字段以维护活跃对象的循环双向链表,同时跟踪总分配数;退出时打印所有现存引用(交互模式下每条语句执行后打印)。

九、推荐的第三方工具

文档列出了创建 C、C++、Rust 扩展的主流工具,它们既提供更简单的路径,也提供更精细的控制:

工具 语言
Cython Cython
cffi C
HPy C
nanobind C++
Numba Cython/Python
pybind11 C++
PyO3 Rust
SWIG 多语言

使用这类工具的好处:避免写出与某个 CPython 版本强绑定的代码,避免引用计数错误,让你聚焦于自己的业务代码而非 CPython API 细节。新 Python 版本通常只需升级工具即可支持,代码往往自动用上更新、更高效的新 API;部分工具还支持从同一份源码编译出 Python 其他实现(非 CPython)的扩展。

重要提醒:这些项目不由维护 Python 的同一批人支持,问题需直接向各项目提报;并且要自行确认项目仍在积极维护——文档中的列表可能过时。另可参考 Python 打包用户指南中关于二进制扩展的章节(讨论可用工具与“为什么值得写扩展模块”)。

十、要点速查

  1. 包含顺序#define PY_SSIZE_T_CLEAN#include <Python.h>,且必须先于一切标准头文件;显式包含自己用到的标准头,不依赖隐式包含。
  2. 命名空间:绝不定义 Py* / _Py* 前缀的符号;绝不使用 _Py* 内部 API。
  3. 引用所有权:通用函数(PyObject_/PyNumber_/PySequence_/PyMapping_)返回新引用;PyList_GetItem/PyTuple_GetItem 类只读访问返回借用引用;PyList_SetItem/PyTuple_SetItem窃取引用。
  4. 错误处理:每次调用后检查 NULL/-1,异常上抛而非覆盖;清理统一走 goto error + Py_XDECREF,拥有引用变量初始化为 NULL,返回值初始化为失败值。
  5. 嵌入三件套Py_InitializeFromConfig(配好 PyConfig.argv/program_name,理解 PYTHONHOME/PYTHONPATH 覆盖关系)、Py_IsInitialized 查询状态、Py_FinalizeEx 终结(但不保证释放扩展模块内存)。
  6. 调试排障--with-pydebug(Unix)或 -d(Windows)启用 Py_DEBUG;引用计数疑难用 --with-trace-refs,完整构建清单见 Misc/SpecialBuilds.txt
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
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
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
983
503
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384