Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 620 – C API 隐藏实现细节

作者:
Victor Stinner <vstinner at python.org>
状态:
已撤回
类型:
标准跟踪
创建日期:
2020年6月19日
Python 版本:
3.12

目录

摘要

引入 C API 不兼容更改以隐藏实现细节。

一旦大多数实现细节被隐藏,CPython 内部的演进将不再受到 C API 向后兼容性问题的限制。添加新功能将变得更加容易。

这使得在 CPython 中进行比微优化更高级的优化实验成为可能,例如标签指针。

定义一个流程以减少损坏的 C 扩展的数量。

此 PEP 的实现预计将在多个 Python 版本中谨慎完成。它已在 Python 3.7 中开始,大部分更改已完成。减少损坏的 C 扩展数量的流程规定了节奏。

PEP 已撤销

此 PEP 已被作者撤销,因为范围太广,且工作分布在多个 Python 版本中,这使得对整个 PEP 做出决定变得困难。它被拆分为具有更窄范围和更好定义的新 PEP,例如 PEP 670

动机

C API 阻碍 CPython 的演进

添加或删除 C 结构体的成员会导致多个向后兼容性问题。

添加新成员会破坏稳定的 ABI(PEP 384),特别是对于静态声明的类型(例如 `static PyTypeObject MyType = {...};`)。在 Python 3.4 中,PEP 442 “安全的析构” 在 `PyTypeObject` 结构体的末尾添加了 `tp_finalize` 成员。为了 ABI 向后兼容性,需要一个新的 `Py_TPFLAGS_HAVE_FINALIZE` 类型标志来声明类型结构是否包含 `tp_finalize` 成员。该标志在 Python 3.8 中被删除(bpo-32388)。

自 2009 年发布的 Python 3.0 起,`PyTypeObject.tp_print` 成员已被弃用,并在 Python 3.8 的开发周期中被删除。但此更改破坏了太多 C 扩展,不得不在 3.8 正式发布前恢复。最终,该成员在 Python 3.9 中再次被删除。

C 扩展依赖于通过 C API 间接访问结构体成员,甚至直接访问。修改 `PyListObject` 这样的结构体是不可考虑的。

`PyTypeObject` 结构体是演变最多的结构体,仅仅因为没有其他方法来演进 CPython 了。

C 扩展技术上可以解引用 `PyObject*` 指针并访问 `PyObject` 成员。这阻碍了诸如标签指针(将小值存储为不指向有效 `PyObject` 结构的 `PyObject*`)之类的实验。

用追踪式垃圾收集器替换 Python 的垃圾收集器也需要移除 `PyObject.ob_refcnt` 引用计数器,而目前 `Py_INCREF()` 和 `Py_DECREF()` 宏直接访问 `PyObject.ob_refcnt`。

自1990年以来 CPython 设计不变:结构体和引用计数

CPython 项目创建时,遵循一个原则:保持实现足够简单,以便单个开发人员可以维护。CPython 的复杂性急剧增加,并实现了许多微优化,但 CPython 的核心设计并未改变。

`PyObject` 和 `PyTupleObject` 结构体的成员自“初始修订”提交(1990 年)以来未发生变化

#define OB_HEAD \
    unsigned int ob_refcnt; \
    struct _typeobject *ob_type;

typedef struct _object {
    OB_HEAD
} object;

typedef struct {
    OB_VARHEAD
    object *ob_item[1];
} tupleobject;

只有名称发生了变化:`object` 被重命名为 `PyObject`,`tupleobject` 被重命名为 `PyTupleObject`。

CPython 内部和第三方 C 扩展(通过 Python C API)仍然使用引用计数来跟踪 Python 对象的生命周期。

所有 Python 对象都必须分配在堆上,并且不能移动。

为什么 PyPy 比 CPython 更高效?

PyPy 项目是一个 Python 实现,平均比 CPython 快 4.2 倍。PyPy 开发人员选择不分叉 CPython,而是从头开始,以便在优化选择方面拥有更大的自由度。

PyPy 不使用引用计数,而是使用追踪式垃圾收集器,该收集器可以移动对象。对象可以分配在栈上(甚至根本不分配),而不是总是必须分配在堆上。

对象布局以性能为中心进行设计。例如,列表策略直接存储整数作为整数,而不是对象。

此外,PyPy 还拥有一个 JIT 编译器,它凭借高效的 PyPy 设计生成快速代码。

PyPy 的瓶颈:Python C API

虽然 PyPy 在运行纯 Python 代码方面比 CPython 高效得多,但在运行 C 扩展方面,它的效率与 CPython 相当或更低。

由于 C API 需要 `PyObject*` 并允许直接访问结构体成员,PyPy 必须将 CPython 对象与 PyPy 对象关联起来并保持两者一致。将 PyPy 对象转换为 CPython 对象效率低下。此外,引用计数也必须在 PyPy 追踪式垃圾收集器之上实现。

进行这些转换是因为 Python C API 与 CPython 实现过于接近:没有高级抽象。例如,结构体成员是公共 C API 的一部分,没有什么可以阻止 C 扩展直接获取或设置 `PyTupleObject.ob_item[0]`(元组的第一个元素)。

有关更多详细信息,请参阅 Antonio Cuni 在 2018 年 9 月发布的 Inside cpyext: Why emulating CPython C API is so Hard

基本原理

隐藏实现细节

从 C API 隐藏实现细节有多个好处

  • 这使得在 CPython 中进行比微优化更高级的优化实验成为可能。例如,标签指针,以及用可以移动对象的追踪式垃圾收集器替换垃圾收集器。
  • 在 CPython 中添加新功能变得更容易。
  • PyPy 应该能够在更多情况下避免转换为 CPython 对象:保留高效的 PyPy 对象。
  • 更容易为新的 Python 实现实现 C API。
  • 更多的 C 扩展将与其他 Python 实现兼容,而不仅仅是 CPython。

与受限 C API 的关系

`PEP 384` “定义稳定的 ABI” 在 Python 3.4 中实现。它引入了“受限 C API”:C API 的一个子集。当使用受限 C API 时,只需构建一次 C 扩展,即可在多个 Python 版本上使用:这就是稳定的 ABI。

PEP 384 的主要限制是 C 扩展必须选择加入受限 C API。只有极少数项目做出了这个选择,通常是为了简化二进制文件的分发,尤其是在 Windows 上。

此 PEP 将 C API 推向受限 C API。

理想情况下,C API 将成为受限 C API,所有 C 扩展都将使用稳定的 ABI,但这超出了此 PEP 的范围。

规范

总结

  • (已完成) 重组 C API 头文件:创建 `Include/cpython/` 和 `Include/internal/` 子目录。
  • (已完成) 将暴露实现细节的私有函数移至内部 C API。
  • (已完成) 将宏转换为静态内联函数。
  • (已完成) 添加新函数 `Py_SET_TYPE()`, `Py_SET_REFCNT()` 和 `Py_SET_SIZE()`。`Py_TYPE()`, `Py_REFCNT()` 和 `Py_SIZE()` 宏变为函数,不能用作左值。
  • (已完成) 新的 C API 函数不得返回借用引用。
  • (进行中) 提供 `pythoncapi_compat.h` 头文件。
  • (进行中) 使结构体不透明,添加 getter 和 setter 函数。
  • (未开始) 弃用 `PySequence_Fast_ITEMS()`。
  • (未开始) 将 `PyTuple_GET_ITEM()` 和 `PyList_GET_ITEM()` 宏转换为静态内联函数。

重组 C API 头文件

C API 的第一个使用者是 Python 本身。API 之间没有明确的区分:有些 API 是必须在 Python 外部使用的,有些 API 是故意公开的。

头文件必须重组为 3 种 API

  • `Include/` 目录是受限 C API:不包含实现细节,结构体不透明。使用它的 C 扩展获得稳定的 ABI。
  • `Include/cpython/` 目录是 CPython C API:不太“可移植”的 API,更依赖于 Python 版本,暴露一些实现细节,可能会发生少量不兼容的更改。
  • `Include/internal/` 目录是内部 C API:实现细节,在每个 Python 版本中都可能发生不兼容的更改。

创建 `Include/cpython/` 目录是完全向后兼容的。`Include/cpython/` 头文件不能直接包含,当 `Py_LIMITED_API` 宏未定义时,它们会自动被 `Include/` 头文件包含。

内部 C API 已安装,可用于特定用途,如调试器和分析器,它们必须在不执行代码的情况下访问结构体成员。使用内部 C API 的 C 扩展与 Python 版本紧密耦合,并且必须在每个 Python 版本中重新编译。

状态:已完成(在 Python 3.8 中)

头文件重组始于 Python 3.7,并在 Python 3.8 中完成。

  • bpo-35134:添加新的 Include/cpython/ 子目录用于“CPython API”,包含实现细节。
  • bpo-35081:将内部头文件移至 `Include/internal/`

将私有函数移至内部 C API

暴露实现细节的私有函数必须移至内部 C API。

如果 C 扩展依赖于暴露 CPython 实现细节的 CPython 私有函数,那么其他 Python 实现必须重新实现此私有函数以支持该 C 扩展。

状态:已完成(在 Python 3.9 中)

私有函数已在 Python 3.8 中移至内部 C API

  • `_PyObject_GC_TRACK()`, `_PyObject_GC_UNTRACK()`

在 Python 3.9 中从受限 C API 中排除的宏和函数

  • `_PyObject_SIZE()`, `_PyObject_VAR_SIZE()`
  • PyThreadState_DeleteCurrent()
  • `PyFPE_START_PROTECT()`, `PyFPE_END_PROTECT()`
  • `_Py_NewReference()`, `_Py_ForgetReference()`
  • _PyTraceMalloc_NewReference()
  • _Py_GetRefTotal()

私有函数已在 Python 3.9 中移至内部 C API

  • GC 函数,如 `_Py_AS_GC()`, `_PyObject_GC_IS_TRACKED()` 和 `_PyGCHead_NEXT()`
  • `_Py_AddToAllObjects()`(未导出)
  • `_PyDebug_PrintTotalRefs()`, `_Py_PrintReferences()`, `_Py_PrintReferenceAddresses()`(未导出)

公共“清除空闲列表”函数已移至内部 C API,并在 Python 3.9 中重命名为私有函数

  • PyAsyncGen_ClearFreeLists()
  • PyContext_ClearFreeList()
  • PyDict_ClearFreeList()
  • PyFloat_ClearFreeList()
  • PyFrame_ClearFreeList()
  • PyList_ClearFreeList()
  • PyTuple_ClearFreeList()
  • 已删除的函数
    • `PyMethod_ClearFreeList()` 和 `PyCFunction_ClearFreeList()`:绑定方法空闲列表在 Python 3.9 中被移除。
    • `PySet_ClearFreeList()`:集合空闲列表在 Python 3.4 中被移除。
    • `PyUnicode_ClearFreeList()`:Unicode 空闲列表在 Python 3.3 中被移除。

将宏转换为静态内联函数

将宏转换为静态内联函数有多个好处

  • 函数具有明确定义的参数类型和返回类型。
  • 函数可以使用具有明确作用域(函数内)的变量。
  • 调试器可以在函数上设置断点,分析器可以在调用堆栈中显示函数名称。在大多数情况下,即使静态内联函数被内联,这也能正常工作。
  • 函数没有 宏陷阱

将宏转换为静态内联函数只会影响极少数以非正常方式使用宏的 C 扩展。

为了向后兼容,函数必须继续接受任何类型,而不仅仅是 `PyObject*`,以避免编译器警告,因为大多数宏都会将其参数强制转换为 `PyObject*`。

Python 3.6 要求 C 编译器支持静态内联函数:PEP 7 要求 C99 的一个子集。

状态:已完成(在 Python 3.9 中)

宏已在 Python 3.8 中转换为静态内联函数

  • `Py_INCREF()`, `Py_DECREF()`
  • `Py_XINCREF()`, `Py_XDECREF()`
  • `PyObject_INIT()`, `PyObject_INIT_VAR()`
  • `_PyObject_GC_TRACK()`, `_PyObject_GC_UNTRACK()`, `_Py_Dealloc()`

宏已在 Python 3.9 中转换为常规函数

  • `Py_EnterRecursiveCall()`, `Py_LeaveRecursiveCall()`(已添加到受限 C API)
  • `PyObject_INIT()`, `PyObject_INIT_VAR()`
  • PyObject_GET_WEAKREFS_LISTPTR()
  • PyObject_CheckBuffer()
  • PyIndex_Check()
  • PyObject_IS_GC()
  • `PyObject_NEW()`(别名至 `PyObject_New()`),`PyObject_NEW_VAR()`(别名至 `PyObject_NewVar()`)
  • `PyType_HasFeature()`(始终调用 `PyType_GetFlags()`)
  • `Py_TRASHCAN_BEGIN_CONDITION()` 和 `Py_TRASHCAN_END()` 宏现在调用隐藏实现细节的函数,而不是直接访问 `PyThreadState` 结构体的成员。

使结构体不透明

C API 的以下结构体变为不透明

  • PyInterpreterState
  • PyThreadState
  • PyGC_Head
  • PyTypeObject
  • `PyObject` 和 `PyVarObject`
  • PyTypeObject
  • 所有继承自 `PyObject` 或 `PyVarObject` 的类型

C 扩展必须使用 getter 或 setter 函数来获取或设置结构体成员。例如,`tuple->ob_item[0]` 必须替换为 `PyTuple_GET_ITEM(tuple, 0)`。

为了能够摆脱引用计数,`PyObject` 必须变得不透明。目前,引用计数器 `PyObject.ob_refcnt` 在 C API 中暴露。所有结构体都必须变得不透明,因为它们“继承”自 PyObject。例如,`PyFloatObject` 继承自 `PyObject`。

typedef struct {
    PyObject ob_base;
    double ob_fval;
} PyFloatObject;

将 `PyObject` 完全不透明化需要将 `Py_INCREF()` 和 `Py_DECREF()` 宏转换为函数调用。此更改会影响性能。这很可能是使结构体不透明的最后几个更改之一。

使 `PyTypeObject` 结构体不透明会破坏声明静态类型的 C 扩展(例如 `static PyTypeObject MyType = {...};`)。C 扩展必须改为使用 `PyType_FromSpec()` 来在堆上分配类型。使用堆类型还有其他好处,例如与子解释器兼容。结合 PEP 489“多阶段扩展模块初始化”,它使 C 扩展的行为更接近 Python 模块,例如允许创建多个模块实例。

使 `PyThreadState` 结构体不透明需要为 C 扩展使用的成员添加 getter 和 setter 函数。

状态:进行中(始于 Python 3.8)

`PyInterpreterState` 结构体在 Python 3.8 中被设为不透明(bpo-35886),`PyGC_Head` 结构体(bpo-40241)在 Python 3.9 中被设为不透明。

跟踪准备 C API 使以下结构体不透明的工作的问题

  • `PyObject`: bpo-39573
  • `PyTypeObject`: bpo-40170
  • `PyFrameObject`: bpo-40421
    • Python 3.9 添加了 `PyFrame_GetCode()` 和 `PyFrame_GetBack()` getter 函数,并将 `PyFrame_GetLineNumber` 移至受限 C API。
  • `PyThreadState`: bpo-39947
    • Python 3.9 添加了 3 个 getter 函数:`PyThreadState_GetFrame()`, `PyThreadState_GetID()`, `PyThreadState_GetInterpreter()`。

禁止将 Py_TYPE() 用作左值

`Py_TYPE()` 函数获取对象的类型,即其 `PyObject.ob_type` 成员。它实现为一个宏,可以作为左值来设置类型:`Py_TYPE(obj) = new_type`。此代码依赖于 `PyObject.ob_type` 可以直接修改的假设。这阻止了 `PyObject` 结构体变得不透明。

添加了新的 setter 函数 `Py_SET_TYPE()`, `Py_SET_REFCNT()` 和 `Py_SET_SIZE()`,必须改用它们。

`Py_TYPE()`, `Py_REFCNT()` 和 `Py_SIZE()` 宏必须转换为不能用作左值的静态内联函数。

例如,`Py_TYPE()` 宏

#define Py_TYPE(ob)             (((PyObject*)(ob))->ob_type)

变为

#define _PyObject_CAST_CONST(op) ((const PyObject*)(op))

static inline PyTypeObject* _Py_TYPE(const PyObject *ob) {
    return ob->ob_type;
}

#define Py_TYPE(ob) _Py_TYPE(_PyObject_CAST_CONST(ob))

状态:已完成(在 Python 3.10 中)

Python 3.9 中添加了新的函数 `Py_SET_TYPE()`, `Py_SET_REFCNT()` 和 `Py_SET_SIZE()`。

在 Python 3.10 中,`Py_TYPE()`, `Py_REFCNT()` 和 `Py_SIZE()` 不再可以用作左值,必须改用新的 setter 函数。

新的 C API 函数不得返回借用引用

当函数返回借用引用时,Python 无法跟踪调用者何时停止使用该引用。

例如,如果 Python 的 `list` 类型针对小整数进行了优化,直接存储“原始”数字而不是 Python 对象,那么 `PyList_GetItem()` 就必须创建一个临时的 Python 对象。问题在于何时可以安全地删除临时对象。

一般准则是不鼓励为新的 C API 函数返回借用引用。

此 PEP 不打算移除任何返回借用引用的函数。

状态:已完成(在 Python 3.9 中)

在 Python 3.9 中,新的 C API 函数返回的 Python 对象仅返回强引用

  • PyFrame_GetBack()
  • PyFrame_GetCode()
  • PyObject_CallNoArgs()
  • PyObject_CallOneArg()
  • PyThreadState_GetFrame()

避免返回 PyObject** 的函数

`PySequence_Fast_ITEMS()` 函数提供对 `PyObject*` 对象数组的直接访问。该函数被弃用,转而使用 `PyTuple_GetItem()` 和 `PyList_GetItem()`。

`PyTuple_GET_ITEM()` 可能被滥用以直接访问 `PyTupleObject.ob_item` 成员

PyObject **items = &PyTuple_GET_ITEM(0);

`PyTuple_GET_ITEM()` 和 `PyList_GET_ITEM()` 宏被转换为静态内联函数以禁止这种行为。

状态:未开始

新的 pythoncapi_compat.h 头文件

使结构体不透明需要修改 C 扩展以使用 getter 和 setter 函数。实际问题是如何保持对不提供这些函数的旧 Python 版本的支持。

例如,在 Python 3.10 中,不再可能将 `Py_TYPE()` 用作左值。必须改用新的 `Py_SET_TYPE()` 函数。

#if PY_VERSION_HEX >= 0x030900A4
    Py_SET_TYPE(&MyType, &PyType_Type);
#else
    Py_TYPE(&MyType) = &PyType_Type;
#endif

这段代码可能会让那些将 Python 代码库从 Python 2 移植到 Python 3 的开发者感到熟悉。

Python 将分发一个新的 `pythoncapi_compat.h` 头文件,该文件为旧 Python 版本提供了新的 C API 函数。例如:

#if PY_VERSION_HEX < 0x030900A4
static inline void
_Py_SET_TYPE(PyObject *ob, PyTypeObject *type)
{
    ob->ob_type = type;
}
#define Py_SET_TYPE(ob, type) _Py_SET_TYPE((PyObject*)(ob), type)
#endif  // PY_VERSION_HEX < 0x030900A4

使用此头文件,`Py_SET_TYPE()` 也可以在旧 Python 版本上使用。

开发者可以将其复制到他们的项目中,甚至只复制/粘贴他们 C 扩展所需的少数函数。

状态:进行中(已实现但尚未由 CPython 分发)

`pythoncapi_compat.h` 头文件目前正在开发中:https://github.com/pythoncapi/pythoncapi_compat

减少损坏的 C 扩展数量的流程

在引入此 PEP 中列出的 C API 不兼容更改时,减少损坏的 C 扩展数量的流程

  • 估算有多少流行的 C 扩展受不兼容更改的影响。
  • 与损坏的 C 扩展的维护者协调,为未来的不兼容更改准备他们的代码。
  • 在 Python 中引入不兼容的更改。文档必须解释如何移植现有代码。建议在开发周期的开始合并此类更改,以便有更多时间进行测试。
  • 最有可能破坏大量 C 扩展的更改应在 capi-sig 邮件列表中宣布,以通知 C 扩展维护者为下一个 Python 版本准备他们的项目。
  • 如果更改破坏了太多项目,应讨论恢复更改,同时考虑到损坏的包的数量、它们在 Python 社区中的重要性以及更改的重要性。

协调通常意味着向项目报告问题,甚至提出更改。这不需要等待包含已修复所有损坏项目的发布。

由于越来越多的 C 扩展使用 Cython 编写,而不是直接使用 C API,因此确保 Cython 提前为不兼容的更改做好准备很重要。这为 C 扩展维护者提供了更多时间来发布新版本(使用更新的 Cython 生成的代码,针对分发 Cython 生成代码的 C 扩展)。

未来的不兼容更改可以通过在文档中弃用一个函数并用 `Py_DEPRECATED()` 注释函数来宣布。但是,使结构体不透明并阻止宏用作左值无法用 `Py_DEPRECATED()` 弃用。

重要部分是协调和在 CPython 演进与向后兼容性之间找到平衡。例如,破坏 PyPI 上随机、旧的、晦涩难懂且无人维护的 C 扩展比破坏 NumPy 的影响要小。

如果更改被恢复,我们将回到协调步骤,以便更好地准备更改。一旦更多的 C 扩展准备就绪,就可以重新考虑不兼容的更改。

版本历史

  • 版本 3,2020 年 6 月:PEP 从头重写。Python 现在分发新的 `pythoncapi_compat.h` 头文件,并定义了一个流程,以在引入此 PEP 中列出的 C API 不兼容更改时减少损坏的 C 扩展数量。
  • 版本 2,2020 年 4 月:PEP:修改 C API 以隐藏实现细节
  • 版本 1,2017 年 7 月:PEP:在 C API 中隐藏实现细节 发送给 python-ideas

来源:https://github.com/python/peps/blob/main/peps/pep-0620.rst

最后修改:2025-02-01 08:55:40 GMT