首页
/ CPython 3.15 C API 移除清单:被废弃函数的替代方案与源码级解析

CPython 3.15 C API 移除清单:被废弃函数的替代方案与源码级解析

2026-09-06 13:28:18作者:庞眉杨Will

本文基于 CPython 仓库中的废弃计划文档 c-api-pending-removal-in-3.15.rst,系统梳理计划在 Python 3.15 中移除的 12 个 C API 函数,逐一给出官方推荐替代接口、行为差异与注意事项。结合仓库源码(Python/import.cObjects/weakrefobject.cObjects/unicodeobject.cPython/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_ImportModuleNoBlockPyImport_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);
}

可以看到两点:

  1. 调用它会立即触发一次 DeprecationWarning("deprecated and scheduled for removal in Python 3.15"),扩展模块在调试期很容易暴露这个调用;
  2. 实现已退化为 PyImport_ImportModule 的直通包装——所谓"非阻塞"行为在当前实现中已不存在,保留该符号纯粹是为了稳定 ABI 兼容。

迁移方式因此非常简单:直接把调用点替换为 PyImport_ImportModule(name) 即可,返回值语义一致(成功时返回引用计数已加 1 的模块对象,失败返回 NULL 并设置异常)。

三、弱引用:PyWeakref_GetObject / PyWeakref_GET_OBJECTPyWeakref_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 表示成功,此时 *pobjborrowed 对象指针;返回 0 表示引用已失效,*pobj 被置为 NULL(而非旧接口的 Py_None)。判断失效必须检查返回值,不能再用 obj == Py_None 这类写法。
  • 错误处理:类型不匹配时旧函数走 PyErr_BadInternalCall(视为程序员错误、不设置可捕获异常),新接口则会设置 TypeError: expected a weakref,行为更可诊断。
  • 失效判断:若只需判断"是否还活着",可搭配 PyWeakref_IsDeadObjects/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_AsDecodedObjectPyUnicode_AsDecodedUnicodePyUnicode_AsEncodedObjectPyUnicode_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_AsDecodedUnicodePyCodec_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")可能返回 strbytes 之外的其他类型。如果你的代码假设"解码必得 str、编码必得 bytes",迁移后需要自行做 PyUnicode_Check / PyBytes_Check 类型检查,否则可能出现运行期类型错误。

替换示意(encodingNULL 时同样默认使用 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_GetPathPy_GetPrefixPy_GetExecPrefixPy_GetProgramFullPathPy_GetProgramNamePy_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_GetPathPyConfig_Get("module_search_paths"):对应 sys.path,即模块搜索路径。注意上面实现中的细节:用户显式提供的 module_search_path 优先于计算值,这与 sys.path 的语义一致。
  • Py_GetPrefixPyConfig_Get("base_prefix"):对应 sys.base_prefix。若扩展需要感知虚拟环境(venv),应改用 "prefix" 键(对应 sys.prefix)——虚拟环境下两者不同,base_prefix 始终指向基础解释器。
  • Py_GetExecPrefixPyConfig_Get("base_exec_prefix"):对应 sys.base_exec_prefix;需处理虚拟环境时改用 "exec_prefix"(对应 sys.exec_prefix)。
  • Py_GetProgramFullPathPy_GetProgramNamePyConfig_Get("executable"):两者统一映射到 sys.executable。原函数一个返回完整路径、一个只返回程序名,替代后需自行按需求做路径/文件名处理。
  • Py_GetPythonHomePyConfig_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 的迁移检查清单

  1. 全局搜索调用点:在扩展模块源码中检索 PyImport_ImportModuleNoBlockPyWeakref_GetObjectPyWeakref_GET_OBJECTPyUnicode_AsDecoded*PyUnicode_AsEncoded* 以及 6 个 Py_Get* 函数。
  2. 逐条替换:按上文映射表换用 PyImport_ImportModulePyWeakref_GetRefPyCodec_Decode/EncodePyConfig_Get
  3. 重点复查两类语义变化PyWeakref_GetRef 的返回值/出参语义(失效时为 NULL 而非 Py_None);PyCodec_* 的返回值类型不再保证是 str/bytes
  4. 验证兼容基线:若目标运行时仍含 3.13 及以下版本,确认是否引入 pythoncapi-compat 以获取 PyWeakref_GetRefPyConfig_Get;而"稳定 ABI 保留"机制保证已按旧符号链接的二进制在 3.15 上仍可运行,但新代码不应再引入这些符号。

以上结论均可在当前仓库中复核:废弃清单见 Doc/deprecations/c-api-pending-removal-in-3.15.rst,实现细节分别位于 Python/import.cObjects/weakrefobject.cObjects/unicodeobject.cPython/pathconfig.c

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