作者:
Petr Viktorin <encukou at gmail.com>
讨论链接:
Discourse thread
状态:
草案
类型:
标准追踪
创建日期:
2025-12-19
Python版本:
3.15
发布历史:
2026-01-06
用一个新的、类型安全性更高的结构替换类型槽和模块槽,该结构允许以更具前向兼容性的方式添加新槽。现有的槽结构及相关API将被软弃用。(即:它将继续工作且不会产生警告,并将被完整记录和支持,但计划不再为其添加任何新功能。)
Python 3.14的C API包含两个可扩展的结构体,用于在创建新对象时提供信息:PyType_Spec 和 PyModuleDef。每个都有一系列C API函数用于从中创建Python对象。这些是:
PyType_Spec 的 PyType_From* 函数,如 PyType_FromMetaclass();PyModuleDef 的 PyModule_FromDef* 函数,如 PyModule_FromDefAndSpec()。将“输入”结构与运行时对象分离,使得对象的内部结构可以保持不透明(在API和ABI层面),允许未来的CPython版本(甚至其他实现)更改细节。这两个结构都包含一个 slots 字段——本质上是一个标记联合数组,允许未来扩展。(实际上,槽是使用整数ID标记的void指针。)在PEP 793中,添加了新的模块创建API。它不再使用 PyModuleDef 结构,而是只使用一个槽数组。为了替换 PyModuleDef 的现有成员,它添加了相应的槽ID——例如,模块名称在 Py_mod_name 槽中指定,而不是在 PyModuleDef.m_name 中。该PEP指出:
PyModuleDef_Slot结构体与固定字段相比确实有一些缺点。我们相信这些是可以修复的,但将此问题留作本PEP的范围之外。本提案旨在解决这些缺点。
现有 PyModuleDef_Slot 和 PyType_Slot 的主要缺点有:
void * 用于数据指针、函数指针和小整数,需要进行强制转换,这在所有相关架构上实践可行,但从技术上讲是C语言中未定义或实现定义的行为。例如:Py_tp_doc 标记一个字符串;Py_mod_gil 标记一个整数;Py_tp_repr 标记一个函数;所有这些都必须强制转换为 void*。Py_mod_gil 和 Py_mod_multiple_interpreters 就是很好的例子。一种解决方法是检查Python版本,并省略早于当前解释器的槽。这对用户来说很麻烦。它也限制了C API可能的非CPython实现,阻止它们“挑选”较新CPython版本中引入的功能。本提案添加了从槽数组创建类和模块的API,这些槽可以使用宏指定为C字面量,如下所示:
static PySlot myClass_slots[] = {
PySlot_STATIC(tp_name, "mymod.MyClass"),
PySlot_SIZE(tp_extra_basicsize, sizeof(struct myClass)),
PySlot_FUNC(tp_repr, myClass_repr),
PySlot_INT64(tp_flags, Py_TPFLAGS_DEFAULT | Py_TPFLAGS_MANAGED_DICT),
PySlot_END,
};
// ...
PyObject *MyClass = PyType_FromSlots(myClass_slots, -1);宏简化了手写字面量。对于更复杂的用例,例如多个Python版本之间的兼容性,或模板化/自动生成的槽数组,以及C API的非C用户,可以写出槽结构体定义。例如,如果从 tp_getattr 到 tp_getattro 的过渡发生在不久的将来(比如CPython 3.17),而不是1.4,并且用户想要支持带有和不带有 tp_getattro 的CPython,他们可以添加一个“HAS_FALLBACK”标志:
static PySlot myClass_slots[] = {
...
{ // 如果不支持则跳过
.sl_id=Py_tp_getattro,
.sl_flags=PySlot_HAS_FALLBACK,
.sl_func=myClass_getattro,
},
{ // 如果上面的槽被跳过则使用这个
.sl_id=Py_tp_getattr,
.sl_func=myClass_old_getattr,
},
PySlot_END,
};类似地,如果 nb_matrix_multiply 槽(PEP 465)在不久的将来被添加,用户可以添加一个带有“OPTIONAL”标志的槽,使他们的类仅在有该操作符的CPython版本上支持 @ 操作符。
这里我们解释本提案中的设计决策。部分原理与PEP 793重复,该PEP用槽数组替换了 PyModuleDef 结构体。
使用槽
槽的主要替代方案是使用版本化结构体作为输入。这种设计有两种变体:
PyTypeObject 中看到的,这种结构体的大部分在实践中往往是NULL。随着更多字段变得过时,要么浪费增长,要么我们引入新的结构体布局(同时在一段时间内保持与旧布局的兼容性)。仅使用槽
类 PyType_Spec 和 PyModuleDef 除了槽数组外还有显式字段。这些包括:
PyType_Spec.name)。本提案为名称添加了一个槽ID,并使其成为必需。basicsize, flags)。最初,槽只包含函数指针;它们现在也包含数据指针以及整数或标志。本提案使用联合体来干净地处理类型。PyModuleDef.m_slots 本身是从总是为NULL的 m_reload 重新利用而来的;可选的 m_traverse 或 m_methods 成员早于它。我们可以没有这些字段,只保留一个槽数组。围绕数组的包装类会使设计复杂化。如果此类中的字段变得过时,它们很难被移除或重新利用。
嵌套槽表
在本提案中,槽数组可以引用另一个槽数组,后者被视为递归地合并到其“父”数组中。这使解释器内部的槽处理复杂化,但允许:
PyType_From* 系列函数扩展的问题,这些值通常不能是静态的(即在Windows上,它通常是来自另一个DLL的符号,不能是静态数据)。嵌套“旧式”槽表
类似于嵌套 PyType_Slot 数组,我们还提议在“新”槽中支持“旧式”槽(PyType_Slot 和 PyModuleDef_Slot)的数组,反之亦然。这样,用户可以重用他们已经编写的代码而无需重写/重新格式化,并且仅在需要任何新功能时才使用“新”槽。
固定宽度整数
本提案使用固定宽度整数(uint16_t)作为槽ID和标志。使用C的 int 类型,使用超过16位将不可移植,但在常见平台上会默默地工作。使用 int 但避免超过 UINT16_MAX 的值在常见平台上会浪费16位。将这些定义为 uint16_t 后,除了指针和大小之外的所有内容都使用固定宽度整数似乎是自然的。
内存布局
在常见的64位平台上,我们可以保持新结构体的大小与现有的 PyType_Slot 和 PyModuleDef_Slot 相同。(现有的结构体由于 int 的可移植性和填充而浪费了16个字节中的6个;本提案将这些位用于新功能。)在32位平台上,本提案要求采用与64位相同的布局,大小是现有结构体的两倍(从8字节到16字节)。对于通常是静态的“配置”数据,这应该没问题。该提案不使用位域和枚举,因为它们的内存表示依赖于编译器,在使用C以外的语言使用API时会导致问题。结构的布局假设类型的对齐方式与其大小匹配。
单一ID空间
目前,模块槽和类型槽的数值有重叠:
Py_bf_getbuffer == Py_mod_create == 1Py_bf_releasebuffer == Py_mod_exec == 2Py_mp_ass_subscript == Py_mod_multiple_interpreters == 3Py_mp_length == Py_mod_gil == 4本提案为两者使用单一的序列,因此未来的槽避免这种重叠。这是为了:
现有的4个重叠意味着我们现在无法达到这些目标,但我们可以以对用户透明的方式逐渐迁移到新的数字ID。主要缺点是任何内部查找表要么会更大(如果我们为类型和模块使用单独的表,它们将包含空白),要么更难管理(如果它们被合并)。
将定义一个新的 PySlot 结构体如下:
typedef struct PySlot {
uint16_t sl_id;
uint16_t sl_flags;
union {
uint32_t _sl_reserved; // 必须为0
};
union {
void *sl_ptr;
void (*sl_func)(void);
Py_ssize_t sl_size;
int64_t sl_int64;
uint64_t sl_uint64;
};
} PySlot;sl_id:一个槽编号,标识槽的作用。sl_flags:标志,定义如下。使用槽的函数
将添加以下函数。它将从给定的槽数组创建相应的Python类型对象:
PyObject *PyType_FromSlots(const PySlot *slots);PyModule_FromSlotsAndSpec 函数(在PEP 793中于CPython 3.15添加)将被更改为接受新的槽结构:
PyObject *PyModule_FromSlotsAndSpec(const PySlot *slots, PyObject *spec)通用槽语义
当槽被传递给应用它们的函数时,该函数不会修改槽数组,也不会修改它(递归地)指向的任何数据。函数完成后,允许用户修改或释放数组及其(递归地)指向的任何数据,除非它被明确标记为“静态”(参见下面的 PySlot_STATIC)。这意味着解释器通常需要复制结构体中的所有数据,包括 char * 文本。
标志
sl_flags 可以设置以下位。未分配的位必须设置为零。
PySlot_OPTIONAL:如果槽ID未知,解释器应完全忽略该槽。(例如,如果现在正在向CPython添加 nb_matrix_multiply,你的类型可以使用这个。)PySlot_STATIC:槽指向的所有数据都是静态分配且恒定的。因此,解释器不需要复制信息。函数指针隐含此标志。该标志甚至适用于槽“间接”指向的数据,除了嵌套槽——参见下面的 Py_slot_subslots——它们可以有自己的 PySlot_STATIC 标志。例如,如果应用于指向 PyMemberDef 结构数组的 Py_tp_members 槽,则整个数组及其元素中的名称和文档字符串必须是静态且恒定的。PySlot_HAS_FALLBACK:如果槽ID未知,解释器将忽略该槽。如果已知,解释器应忽略后续槽,直到(并包括)第一个没有 HAS_FALLBACK 的槽。实际上,带有 HAS_FALLBACK 标志的连续槽,加上它们之后的第一个非 HAS_FALLBACK 槽,形成一个“块”,解释器将只考虑该块中它理解的第一个槽。如果整个块是可选的,它应该以一个带有 OPTIONAL 标志的槽结束。PySlot_IS_PTR:数据存储在 sl_ptr 中,并且必须强制转换为适当的类型。此标志简化了从现有 PyType_Slot 和 PyModuleDef_Slot 的移植,其中所有槽都以这种方式工作。便利宏
以下宏将被添加到API中以简化槽定义:
#define PySlot_DATA(NAME, VALUE) \
{.sl_id=NAME, .sl_ptr=(void*)(VALUE)}
#define PySlot_FUNC(NAME, VALUE) \
{.sl_id=NAME, .sl_func=(VALUE)}
#define PySlot_SIZE(NAME, VALUE) \
{.sl_id=NAME, .sl_size=(VALUE)}
#define PySlot_INT64(NAME, VALUE) \
{.sl_id=NAME, .sl_int64=(VALUE)}
#define PySlot_UINT64(NAME, VALUE) \
{.sl_id=NAME, .sl_uint64=(VALUE)}
#define PySlot_STATIC_DATA(NAME, VALUE) \
{.sl_id=NAME, .sl_flags=PySlot_STATIC, .sl_ptr=(VALUE)}
#define PySlot_END {0}我们还将添加另外两个避免使用指定初始化器的宏,用于C++11兼容代码:
#define PySlot_PTR(NAME, VALUE) \
{NAME, PySlot_IS_PTR, {0}, {(void*)(VALUE)}}
#define PySlot_PTR_STATIC(NAME, VALUE) \
{NAME, PySlot_IS_PTR|Py_SLOT_STATIC, {0}, {(void*)(VALUE)}}嵌套槽表
将添加一个新槽 Py_slot_subslots 以允许嵌套槽表。它的值(sl_ptr)应指向一个 PySlot 结构体数组,这些结构体将被视为当前槽数组的一部分。sl_ptr 可以是 NULL 以表示没有槽。另外两个槽将允许对现有槽结构进行类似的嵌套:
Py_tp_slots 用于 PyType_Slot 数组Py_mod_slots 用于 PyModuleDef_Slot 数组数组中的每个 PyType_Slot 将被转换为 (PySlot){.sl_id=slot, .sl_flags=PySlot_IS_PTR, .sl_ptr=func},PyModuleDef_Slot 类似。初始实现将有一些限制,未来可能会取消:
Py_slot_subslots、Py_tp_slots 和 Py_mod_slots 不能使用 PySlot_HAS_FALLBACK(该标志不能在它们身上设置,也不能在它们之前的槽上设置)。PyType_From*、PyModule_From* 函数为4层,它们将在内部使用一层。)新槽ID
将添加以下新的槽ID,可用于类型和模块定义:
Py_slot_end(定义为0):标记槽数组的结束。忽略 PySlot_INTPTR 和 PySlot_STATIC 标志。不允许与 Py_slot_end 一起使用 PySlot_OPTIONAL 和 PySlot_HAS_FALLBACK 标志。Py_slot_subslots、Py_tp_slots、Py_mod_slots:参见上面的嵌套槽表Py_slot_invalid:被视为未知槽ID。将添加以下新的槽ID以覆盖 PyModuleDef 的现有成员:
Py_tp_name(类型创建必需)Py_tp_basicsize(类型为 Py_ssize_t)Py_tp_extra_basicsize(等同于将 PyType_Spec.basicsize 设置为 -extra_basicsize)Py_tp_itemsizePy_tp_flags将添加以下新的槽ID以覆盖 PyType_FromMetaclass 的参数:
Py_tp_metaclass(用于在元类计算后设置 ob_type)Py_tp_module注意 Py_tp_base 和 Py_tp_bases 已经存在。解释器将同等对待它们:两者都可以指定一个类对象或它们的元组。Py_tp_base 将被软弃用,以支持 Py_tp_bases。在单个定义中同时指定两者将被弃用(目前,Py_tp_bases 会覆盖 Py_tp_base)。这些新槽都不能与 PyType_GetSlot 一起使用。(此限制未来可能会取消,需经C API工作组批准。)
槽重新编号
新的槽ID将具有唯一的数值(即 Py_slot_*、Py_tp_* 和 Py_mod_* 不会共享ID)。编号为1到4的槽(Py_bf_getbuffer…Py_mp_length 和 Py_mod_create…Py_mod_gil)将被重新定义为新的(更大的)数字。旧数字将保留为别名,并在为低于3.15的稳定ABI版本编译时使用。PEP 793中添加的用于 PyType_Spec 成员的槽将被重新编号,使它们具有唯一的ID:
Py_mod_namePy_mod_docPy_mod_state_sizePy_mod_methodsPy_mod_state_traversePy_mod_state_clearPy_mod_state_free软弃用
以下现有函数将被软弃用:
PyType_FromSpecPyType_FromSpecWithBasesPyType_FromModuleAndSpecPyType_FromMetaclassPyModule_FromDefAndSpecPyModule_FromDefAndSpec2PyModule_ExecDef(提醒:软弃用的API不计划移除,不会引发警告,并保持文档化和测试。但是,不会为其添加新功能。)这些函数接受的 PyType_Slot 或 PyModuleDef_Slot 数组可以包含任何槽,包括本PEP中定义的“新”槽。这包括嵌套的“新式”槽(Py_slot_subslots)。
本PEP仅添加API,因此是向后兼容的。
未知
调整“扩展和嵌入”教程以使用此方法。
暂无。
暂无。
暂无。
本文档置于公共领域或CC0-1.0-Universal许可下,以较宽松者为准。FINISHED
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。