CPython 3.15 C API 移除清单:被废弃函数的替代方案与源码级解析
本文基于 CPython 仓库中的废弃计划文档 c-api-pending-removal-in-3.15.rst,系统梳理计划在 Python 3.15 中移除的 12 个 C API 函数,逐一给出官方推荐替代接口、行为差异与注意事项。结合仓库源码(Python/import.c、Objects/weakrefobject.c、Objects/unicodeobject.c、Python/pathconfig.c),你可以掌握迁移扩展模块所需的全部细节,理解每个废弃接口为何被替换、以及"removed in 3.15, but kept for stable ABI compatibility"这一注释背后的兼容机制。
一、废弃机制与"稳定 ABI"的兼容边界
CPython 对 C API 的移除遵循"先弃用、后移除"的流程:函数先被标记为 deprecated 并触发 DeprecationWarning,随后在计划版本中从文档和头文件中移除。阅读源码注释可以发现一个关键事实——被标记为 "removed in 3.15, but kept for stable ABI compatibility"(见 Python/pathconfig.c 中多处注释)的函数,其实现仍保留在代码库中,仅为维持稳定 ABI(Limited API / Stable ABI)二进制兼容,旧版本编译的扩展模块在 3.15 上继续工作不会链接失败。但新代码不应再依赖它们。
完整的"废弃函数 → 替代函数"映射清单如下:
| 被废弃的 C API 函数 | 替代方案 | 备注 |
|---|---|---|
PyImport_ImportModuleNoBlock |
PyImport_ImportModule |
见第二节 |
PyWeakref_GetObject |
PyWeakref_GetRef |
注意引用语义变化,见第三节 |
PyWeakref_GET_OBJECT(宏) |
PyWeakref_GetRef |
同上 |
PyUnicode_AsDecodedObject |
PyCodec_Decode |
见第四节 |
PyUnicode_AsDecodedUnicode |
PyCodec_Decode |
某些 codec(如 base64)可能返回 str 之外的类型 |
PyUnicode_AsEncodedObject |
PyCodec_Encode |
见第四节 |
PyUnicode_AsEncodedUnicode |
PyCodec_Encode |
某些 codec(如 base64)可能返回 bytes 之外的类型 |
Py_GetPath |
PyConfig_Get("module_search_paths") |
对应 sys.path;这组函数自 3.13 起弃用 |
Py_GetPrefix |
PyConfig_Get("base_prefix") |
对应 sys.base_prefix;需处理虚拟环境时用 "prefix"(对应 sys.prefix) |
Py_GetExecPrefix |
PyConfig_Get("base_exec_prefix") |
对应 sys.base_exec_prefix;需处理虚拟环境时用 "exec_prefix"(对应 sys.exec_prefix) |
Py_GetProgramFullPath |
PyConfig_Get("executable") |
对应 sys.executable |
Py_GetProgramName |
PyConfig_Get("executable") |
对应 sys.executable |
Py_GetPythonHome |
PyConfig_Get("home") |
或使用 PYTHONHOME 环境变量 |
说明:上述 6 个
Py_Get*路径查询函数是在 Python 3.13 中被标记弃用的(即"deprecated in Python 3.13"),列入 3.15 移除计划;其余 6 个函数则是在更早版本弃用、如今一并列入移除清单。
二、模块导入:PyImport_ImportModuleNoBlock → PyImport_ImportModule
PyImport_ImportModuleNoBlock 的历史用途是"非阻塞导入":先查 sys.modules,若模块从未加载过则尝试加载,但如果其他线程持有导入锁,则直接抛出 ImportError 而不是阻塞等待。在 3.15 计划中,这个函数被 PyImport_ImportModule 完全取代。
源码中的实现清晰地展示了这一过渡形态。在 Python/import.c 中:
/* Import a module without blocking
*
* At first it tries to fetch the module from sys.modules. ...
* Removed in 3.15, but kept for stable ABI compatibility.
*/
PyAPI_FUNC(PyObject *)
PyImport_ImportModuleNoBlock(const char *name)
{
if (PyErr_WarnEx(PyExc_DeprecationWarning,
"PyImport_ImportModuleNoBlock() is deprecated and scheduled for "
"removal in Python 3.15. Use PyImport_ImportModule() instead.", 1))
{
return NULL;
}
return PyImport_ImportModule(name);
}
可以看到两点:
- 调用它会立即触发一次
DeprecationWarning("deprecated and scheduled for removal in Python 3.15"),扩展模块在调试期很容易暴露这个调用; - 实现已退化为
PyImport_ImportModule的直通包装——所谓"非阻塞"行为在当前实现中已不存在,保留该符号纯粹是为了稳定 ABI 兼容。
迁移方式因此非常简单:直接把调用点替换为 PyImport_ImportModule(name) 即可,返回值语义一致(成功时返回引用计数已加 1 的模块对象,失败返回 NULL 并设置异常)。
三、弱引用:PyWeakref_GetObject / PyWeakref_GET_OBJECT → PyWeakref_GetRef
这是本次清单中行为差异最大的一组替换。原接口 PyWeakref_GetObject 返回被引用对象的一个 borrowed reference(借用引用,调用方不负责释放),失效时返回 Py_None;而 PyWeakref_GET_OBJECT 宏直接读取内部指针,绕过了类型检查。二者的替代接口 PyWeakref_GetRef 采用显式出参,返回语义完全不同。
Objects/weakrefobject.c 中的实现对比:
int
PyWeakref_GetRef(PyObject *ref, PyObject **pobj)
{
if (ref == NULL) {
*pobj = NULL;
PyErr_BadInternalCall();
return -1;
}
if (!PyWeakref_Check(ref)) {
*pobj = NULL;
PyErr_SetString(PyExc_TypeError, "expected a weakref");
return -1;
}
*pobj = _PyWeakref_GET_REF(ref);
return (*pobj != NULL);
}
/* removed in 3.15, but kept for stable ABI compatibility */
PyAPI_FUNC(PyObject *)
PyWeakref_GetObject(PyObject *ref)
{
if (ref == NULL || !PyWeakref_Check(ref)) {
PyErr_BadInternalCall();
return NULL;
}
PyObject *obj = _PyWeakref_GET_REF(ref);
if (obj == NULL) {
return Py_None;
}
Py_DECREF(obj);
return obj; // borrowed reference
}
迁移时需要理解的关键差异:
- 返回值语义:
PyWeakref_GetRef返回int——非 0 表示成功,此时*pobj为 borrowed 对象指针;返回 0 表示引用已失效,*pobj被置为NULL(而非旧接口的Py_None)。判断失效必须检查返回值,不能再用obj == Py_None这类写法。 - 错误处理:类型不匹配时旧函数走
PyErr_BadInternalCall(视为程序员错误、不设置可捕获异常),新接口则会设置TypeError: expected a weakref,行为更可诊断。 - 失效判断:若只需判断"是否还活着",可搭配
PyWeakref_IsDead(Objects/weakrefobject.c),它返回 1/0 且不触碰对象指针。
PyObject *obj = NULL;
if (PyWeakref_GetRef(ref, &obj)) {
/* obj 是 borrowed reference,生命周期由外部保证 */
...
} else {
/* 弱引用已失效(*obj == NULL) */
}
文档同时指出,对于仍需在 Python 3.12 及更早版本上使用 PyWeakref_GetRef 的扩展,可以借助 pythoncapi-compat 兼容层项目来补齐该函数。
四、Unicode 编解码:四个 AsDecoded/AsEncoded* 函数 → PyCodec_Decode / PyCodec_Encode
清单中的 PyUnicode_AsDecodedObject、PyUnicode_AsDecodedUnicode、PyUnicode_AsEncodedObject、PyUnicode_AsEncodedUnicode 四个函数统一由 PyCodec_Decode / PyCodec_Encode 取代。从 Objects/unicodeobject.c 的实现看,这四个函数本身早已是对 PyCodec_* 的薄包装:
PyAPI_FUNC(PyObject *)
PyUnicode_AsDecodedObject(PyObject *unicode,
const char *encoding,
const char *errors)
{
if (!PyUnicode_Check(unicode)) {
PyErr_BadArgument();
return NULL;
}
if (encoding == NULL)
encoding = PyUnicode_GetDefaultEncoding();
/* Decode via the codec registry */
return PyCodec_Decode(unicode, encoding, errors);
}
PyUnicode_AsDecodedUnicode 与 PyCodec_Decode 的唯一实质差别在于它强制结果类型:若解码器返回的不是 str,会抛出 TypeError: "'%.400s' decoder returned '%.400s' instead of 'str'; use codecs.decode() to decode to arbitrary types"。同理,PyUnicode_AsEncodedUnicode 要求结果为 bytes。
迁移时的核心注意点(原文档特别强调):直接改用 PyCodec_Decode / PyCodec_Encode 后,返回值类型不再受约束——某些 codec(例如 "base64")可能返回 str、bytes 之外的其他类型。如果你的代码假设"解码必得 str、编码必得 bytes",迁移后需要自行做 PyUnicode_Check / PyBytes_Check 类型检查,否则可能出现运行期类型错误。
替换示意(encoding 为 NULL 时同样默认使用 PyUnicode_GetDefaultEncoding() 的语义):
/* 旧:PyUnicode_AsEncodedObject(text, "utf-8", "strict") */
PyObject *data = PyCodec_Encode(text, "utf-8", "strict");
if (data == NULL) {
return NULL;
}
/* 使用完毕后 Py_DECREF(data) */
五、路径查询:Py_Get* 函数族 → PyConfig_Get
Py_GetPath、Py_GetPrefix、Py_GetExecPrefix、Py_GetProgramFullPath、Py_GetProgramName、Py_GetPythonHome 这 6 个初始化期路径查询函数自 3.13 起弃用,统一替代方案是 PyConfig_Get("<键名>")。它们当前都位于 Python/pathconfig.c,全部是 _Py_path_config 结构体字段的直通读取,且每处都带有 "removed in 3.15, but kept for stable ABI compatibility" 注释:
/* removed in 3.15, but kept for stable ABI compatibility */
PyAPI_FUNC(wchar_t *)
Py_GetPath(void)
{
/* If the user has provided a path, return that */
if (_Py_path_config.module_search_path) {
return _Py_path_config.module_search_path;
}
/* If we have already done calculations, return the calculated path */
return _Py_path_config.calculated_module_search_path;
}
各键名与 sys 模块属性的对应关系,是迁移时的主要决策依据:
Py_GetPath→PyConfig_Get("module_search_paths"):对应sys.path,即模块搜索路径。注意上面实现中的细节:用户显式提供的module_search_path优先于计算值,这与sys.path的语义一致。Py_GetPrefix→PyConfig_Get("base_prefix"):对应sys.base_prefix。若扩展需要感知虚拟环境(venv),应改用"prefix"键(对应sys.prefix)——虚拟环境下两者不同,base_prefix始终指向基础解释器。Py_GetExecPrefix→PyConfig_Get("base_exec_prefix"):对应sys.base_exec_prefix;需处理虚拟环境时改用"exec_prefix"(对应sys.exec_prefix)。Py_GetProgramFullPath与Py_GetProgramName→PyConfig_Get("executable"):两者统一映射到sys.executable。原函数一个返回完整路径、一个只返回程序名,替代后需自行按需求做路径/文件名处理。Py_GetPythonHome→PyConfig_Get("home"),或直接读取PYTHONHOME环境变量。
wchar_t *exe = PyConfig_Get("executable");
if (exe == NULL) {
return NULL;
}
/* 使用后需 PyMem_Free(exe) 释放(遵循 PyConfig_Get 的内存约定) */
同样地,若扩展需要兼容 Python 3.13 及更早版本,可依赖 pythoncapi-compat 项目获得旧版本上的 PyConfig_Get。
六、小结:面向 3.15 的迁移检查清单
- 全局搜索调用点:在扩展模块源码中检索
PyImport_ImportModuleNoBlock、PyWeakref_GetObject、PyWeakref_GET_OBJECT、PyUnicode_AsDecoded*、PyUnicode_AsEncoded*以及 6 个Py_Get*函数。 - 逐条替换:按上文映射表换用
PyImport_ImportModule、PyWeakref_GetRef、PyCodec_Decode/Encode、PyConfig_Get。 - 重点复查两类语义变化:
PyWeakref_GetRef的返回值/出参语义(失效时为NULL而非Py_None);PyCodec_*的返回值类型不再保证是str/bytes。 - 验证兼容基线:若目标运行时仍含 3.13 及以下版本,确认是否引入
pythoncapi-compat以获取PyWeakref_GetRef、PyConfig_Get;而"稳定 ABI 保留"机制保证已按旧符号链接的二进制在 3.15 上仍可运行,但新代码不应再引入这些符号。
以上结论均可在当前仓库中复核:废弃清单见 Doc/deprecations/c-api-pending-removal-in-3.15.rst,实现细节分别位于 Python/import.c、Objects/weakrefobject.c、Objects/unicodeobject.c 与 Python/pathconfig.c。
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 StartedRust0624
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