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-06-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 的核心设计没有改变。

PyObjectPyTupleObject 结构体的成员自“初始修订”提交(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 重命名为 PyObjecttupleobject 重命名为 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 编写的 Inside cpyext: 为什么模拟 CPython C API 如此困难(2018 年 9 月)。

基本原理

隐藏实现细节

从 C API 中隐藏实现细节具有多个优点

  • 它使得在 CPython 中尝试更高级的优化成为可能,例如标记指针,以及用可以移动对象的跟踪垃圾收集器替换垃圾收集器。
  • 添加 CPython 中的新功能变得更容易。
  • PyPy 应该能够在更多情况下避免转换为 CPython 对象:保持高效的 PyPy 对象。
  • 为新的 Python 实现实现 C API 变得更容易。
  • 更多 C 扩展将与除 CPython 之外的 Python 实现兼容。

与受限 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 本身。在必须不在 Python 外部使用的 API 和有意公开的 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:为包含实现细节的“CPython API”添加一个新的 Include/cpython/ 子目录。
  • 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()(未导出)

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

  • 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():在 Python 3.3 中删除了 Unicode 空闲列表。

将宏转换为静态内联函数

将宏转换为静态内联函数具有多个优点

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

将宏转换为静态内联函数只会影响极少数以不寻常方式使用宏的 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
  • PyObjectPyVarObject
  • PyTypeObject
  • 所有从 PyObjectPyVarObject 继承的类型

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 准备工作以使以下结构变为不透明的问题

  • PyObjectbpo-39573
  • PyTypeObjectbpo-40170
  • PyFrameObjectbpo-40421
    • Python 3.9 添加了 PyFrame_GetCode()PyFrame_GetBack() getter 函数,并将 PyFrame_GetLineNumber 移动到受限 C API。
  • PyThreadStatebpo-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 中)

新的函数 Py_SET_TYPE()Py_SET_REFCNT()Py_SET_SIZE() 已添加到 Python 3.9。

在 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 中,返回 Python 对象的新 C API 函数仅返回强引用

  • 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 不具有这些函数。

例如,在 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

上次修改时间:2023-09-09 17:39:29 GMT