CPython C API 全景指南:头文件组织、引用计数、异常处理与嵌入 Python 的实战详解
本文基于 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。使用它的动机有两类,且性质截然不同:
- 编写扩展模块(extension modules):用 C 代码扩展解释器能力,这是最常见的用途。文档将其描述为“相对成熟、可按菜谱(cookbook)操作”的流程,且有多款自动化工具降低门槛。
- 嵌入 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_prefix由configure脚本的对应参数决定,X.Y为'%d.%d' % sys.version_info[:2]。 - Windows:头文件安装在
{prefix}/include,prefix即安装器指定的安装目录。
关键陷阱:编译时应把这两个目录(若不同)直接放入编译器头文件搜索路径,而不是把父目录放进搜索路径后写 #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_NONE、PyMODINIT_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 新增):用于“设计上不可能到达”的代码路径,典型场景是switch的default:分支(所有取值已被case覆盖)。发布模式下它帮助编译器优化并消除不可达告警——GCC 上实现为__builtin_unreachable(),MSVC 上为__assume(0),这一点可在 Include/pymacro.h 中逐分支看到;调试模式与不支持的编译器上则展开为Py_FatalError调用。注意:对“很罕见但确实可能到达”的路径(低内存、系统调用返回意外值)绝不能使用它,应把错误上报给调用者或改用Py_FatalError。 -
Py_SAFE_DOWNCAST(value, larger, smaller):把value从larger转为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 软废弃):printf的size_t修饰符,直接用"z"。Py_LL(number)/Py_ULL(number)(3.15 软废弃):C99 起直接写LL/LLU后缀即可。PY_LONG_LONG、PY_INT32_T、PY_UINT32_T、PY_INT64_T、PY_UINT64_T(3.15 软废弃):分别是long long、int32_t等标准类型的别名,直接使用标准类型。PY_LLONG_MIN、PY_LLONG_MAX、PY_ULLONG_MAX、PY_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.h 中 Py_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_SetItem 与 PyTuple_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_SetItem与PyObject_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_GetItem、PySequence_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 类型(int、long、double、char*)外,少数结构体类型用于描述模块导出函数表、新类型的数据属性表等静态表,另有复杂数值的值类型——这些随使用它们的函数一起讨论。最重要的独立类型是:
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用于检查:有异常时返回异常类型对象的借用引用,否则返回NULL。PyErr_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 编程的几条“官方背书”惯例:
goto用于错误清理是被认可的正确用法——成功与失败路径共享同一段清理代码;PyErr_ExceptionMatches+PyErr_Clear:精确处理特定异常(这里只捕获KeyError),其余异常直接上抛;Py_XDECREF(名字里的X即“可空”):释放可能为 NULL 的拥有引用——若用Py_DECREF遇到 NULL 会崩溃;因此所有持有拥有引用的变量都初始化为NULL;- 返回值先初始化为失败值(-1),只有最后一次调用成功后才置为 0。
七、嵌入 Python(Embedding)
嵌入者(相对扩展作者而言)独有的核心任务只有一件:解释器的初始化(以及可能的终结)——解释器的大多数功能只有在初始化之后才能使用。
7.1 基本初始化
Py_Initialize:初始化已加载模块表,创建三个基本模块builtins、__main__、sys,并初始化模块搜索路径(sys.path)。- 注意:
Py_Initialize不会设置sys.argv。若后续执行的 Python 代码需要它,必须设置PyConfig.argv与PyConfig.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。 - 除下文引用计数调试外还有额外检查。
- Unix:在
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 打包用户指南中关于二进制扩展的章节(讨论可用工具与“为什么值得写扩展模块”)。
十、要点速查
- 包含顺序:
#define PY_SSIZE_T_CLEAN→#include <Python.h>,且必须先于一切标准头文件;显式包含自己用到的标准头,不依赖隐式包含。 - 命名空间:绝不定义
Py*/_Py*前缀的符号;绝不使用_Py*内部 API。 - 引用所有权:通用函数(
PyObject_/PyNumber_/PySequence_/PyMapping_)返回新引用;PyList_GetItem/PyTuple_GetItem类只读访问返回借用引用;PyList_SetItem/PyTuple_SetItem会窃取引用。 - 错误处理:每次调用后检查
NULL/-1,异常上抛而非覆盖;清理统一走goto error+Py_XDECREF,拥有引用变量初始化为NULL,返回值初始化为失败值。 - 嵌入三件套:
Py_InitializeFromConfig(配好PyConfig.argv/program_name,理解PYTHONHOME/PYTHONPATH覆盖关系)、Py_IsInitialized查询状态、Py_FinalizeEx终结(但不保证释放扩展模块内存)。 - 调试排障:
--with-pydebug(Unix)或-d(Windows)启用Py_DEBUG;引用计数疑难用--with-trace-refs,完整构建清单见 Misc/SpecialBuilds.txt。
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