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 的核心设计没有改变。
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 编写的 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 中完成。
将私有函数移动到内部 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
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 准备工作以使以下结构变为不透明的问题
不允许使用 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