PEP 573 – 从 C 扩展方法访问模块状态
- 作者:
- Petr Viktorin <encukou at gmail.com>, Alyssa Coghlan <ncoghlan at gmail.com>, Eric Snow <ericsnowcurrently at gmail.com>, Marcel Plch <gmarcel.plch at gmail.com>
- BDFL-委托:
- Stefan Behnel
- 讨论邮件列表:
- Import-SIG 邮件列表
- 状态:
- 最终
- 类型:
- 标准跟踪
- 创建日期:
- 2016-06-02
- Python 版本:
- 3.9
- 发布历史:
摘要
此 PEP 提出添加一种方法,使 CPython 扩展方法能够访问上下文,例如它们定义所在的模块的状态。
这将允许扩展方法使用直接指针解引用,而不是使用 `PyState_FindModule` 来查找模块状态,从而减少或消除使用模块范围状态而不是进程全局状态的性能成本。
这修复了 PEP 3121(扩展模块初始化和终结)和 PEP 489(多阶段扩展模块初始化)采用的一项剩余障碍。
虽然此 PEP 采取了进一步的步骤,以完全解决 PEP 3121 和 PEP 489 开始解决的问题,但它并未尝试解决所有剩余的疑虑。特别是,从槽方法(`nb_add` 等)访问模块状态尚未解决。
术语
进程全局状态
C 级静态变量。由于这是非常低级的内存存储,因此必须仔细管理。
每个模块的状态
模块对象本地状态,作为模块对象初始化的一部分动态分配。这将状态与模块的其他实例(包括其他子解释器中的实例)隔离开来。
通过 `PyModule_GetState()` 访问。
静态类型
作为 C 级静态变量定义的类型对象,即编译器类型对象。
静态类型需要在模块实例之间共享,并且没有关于它属于哪个模块的信息。静态类型没有 `__dict__`(尽管它们的实例可能具有)。
堆类型
在运行时创建的类型对象。
定义类
方法(绑定或未绑定)的定义类是定义方法的类。仅仅从其基类继承方法的类不是定义类。
例如,`int` 是 `True.to_bytes`、`True.__floor__` 和 `int.__repr__` 的定义类。
在 C 中,定义类是使用相应的 `tp_methods` 或“tp 槽”[1] 条目定义的类。对于在 Python 中定义的方法,定义类存储在 `__class__` 闭包单元格中。
C-API
Python 文档中描述的“Python/C API”。CPython 实现 C-API,但存在其他实现。
理由
PEP 489 引入了一种新的方式来初始化扩展模块,这为实现它的扩展带来了诸多优势
- 扩展模块的行为更像它们的 Python 对应物。
- 扩展模块可以轻松地支持加载到预先存在的模块对象中,这为扩展模块支持 `runpy` 或允许扩展模块重新加载的系统铺平了道路。
- 可以从同一个扩展加载多个模块,这使得能够从单个解释器测试模块隔离(子解释器正确支持的关键特性)。
采用 PEP 489 的最大障碍是允许从扩展类型的 method 访问模块状态。目前,从扩展方法访问此状态的方法是通过 `PyState_FindModule` 来查找模块(与扩展模块中的模块级函数相反,它们接收模块引用作为参数)。但是,`PyState_FindModule` 查询线程本地状态,与 C 级进程全局访问相比,它的成本相对较高,因此阻止了模块作者使用它。
此外,`PyState_FindModule` 依赖于以下假设:在每个子解释器中,最多有一个与给定 `PyModuleDef` 对应的模块。对于使用 PEP 489 的多阶段初始化的模块,此假设不成立,因此这些模块无法使用 `PyState_FindModule`。
需要一种更快、更安全的方式来从扩展方法访问模块级状态。
背景
Python 方法的实现可能需要访问以下一个或多个信息片段
- 它调用的实例 (`self`)
- 底层函数
- 定义类,即定义方法的类
- 相应的模块
- 模块状态
在 Python 代码中,可以检索 Python 级等效项,如下所示
import sys
class Foo:
def meth(self):
instance = self
module_globals = globals()
module_object = sys.modules[__name__] # (1)
underlying_function = Foo.meth # (1)
defining_class = Foo # (1)
defining_class = __class__ # (2)
注意
定义类不是 `type(self)`,因为 `type(self)` 可能是 `Foo` 的子类。
标记为 (1) 的语句隐式地依赖于通过函数的 `__globals__` 进行的基于名称的查找:`Foo` 属性用于访问定义类和 Python 函数对象,或者 `__name__` 用于在 `sys.modules` 中查找模块对象。
在 Python 代码中,这是可行的,因为 `__globals__` 在执行函数定义时被适当地设置,即使命名空间已被操纵以返回不同的对象,最坏的情况也是会引发异常。
`__class__` 闭包 (2) 是一种更安全的方式来获取定义类,但它仍然依赖于 `__closure__` 被适当地设置。
相比之下,扩展方法通常实现为普通的 C 函数。这意味着它们只能访问它们的 arguments 以及 C 级线程本地和进程全局状态。传统上,许多扩展模块将它们共享的状态存储在 C 级进程全局变量中,当以下情况发生时会导致问题
- 在同一进程中运行多个初始化/终结周期
- 重新加载模块(例如,测试条件导入)
- 在子解释器中加载扩展模块
PEP 3121 试图通过提供 `PyState_FindModule` API 来解决这个问题,但这在处理扩展方法(而不是模块级函数)时仍然存在重大问题
- 它明显比直接访问 C 级进程全局状态慢
- 仍然有一些固有的对进程全局状态的依赖,这意味着它仍然无法可靠地处理模块重新加载
当查找 C 级结构(例如模块状态)时,提供意外的对象布局可能会使解释器崩溃,因此确保扩展方法接收它们期望的类型非常重要。
提案
当前,绑定扩展方法 (`PyCFunction` 或 `PyCFunctionWithKeywords`) 仅接收 `self`,以及(如果适用)提供的 positional 和 keyword arguments。
虽然模块级扩展函数已经可以通过它们的 `self` 参数访问定义模块对象,但扩展类型的 method 没有这种便利:它们通过 `self` 接收绑定的实例,因此无法直接访问定义类或模块级状态。
可以通过两个更改来提供上面描述的额外模块级上下文。这两个添加都是可选的;扩展作者需要选择加入才能开始使用它们
- 向堆类型对象添加指向模块的指针。
- 将定义类传递给底层 C 函数。
在 CPython 中,定义类在创建内置 method 对象 (`PyCFunctionObject`) 时 readily available,因此可以将其存储在一个扩展 `PyCFunctionObject` 的新结构中。
然后可以通过 `PyModule_GetState` 从模块对象检索模块状态。
请注意,此提案意味着任何需要访问 每个模块的状态 的类型都必须是堆类型,而不是静态类型。这对于支持从单个扩展加载多个模块对象是必需的:静态类型(作为 C 级全局变量)没有关于它属于哪个模块对象的信息。
槽方法
以上更改不涵盖槽方法,例如 `tp_iter` 或 `nb_add`。
槽方法的问题在于它们的 C API 是固定的,因此我们不能简单地添加一个新参数来传递定义类。已经提出了两个可能的解决方案来解决这个问题
- 通过遍历 MRO 来查找类。这可能很昂贵,但如果性能不是问题(例如,在引发模块级异常时),它将可以使用。
- 在单独的表
__typeslots__
[2] 中存储指向每个槽位定义类的指针。从技术上讲,这是可行的并且速度很快,但入侵性很大。
受此问题影响的模块还可以选择使用 线程本地状态 或 PEP 567 上下文变量 作为缓存机制,或者定义自己的 reload 友好查找缓存方案。
通常,解决此问题的方案将在未来的 PEP 中进行讨论。
规范
向堆类型添加模块引用
将在 C-API 中添加一个新的工厂方法来创建模块。
PyObject* PyType_FromModuleAndSpec(PyObject *module,
PyType_Spec *spec,
PyObject *bases)
此方法的作用与 PyType_FromSpecWithBases
相同,另外还会将提供的模块对象与新类型相关联。(在 CPython 中,这将设置下面描述的 ht_module
)。
此外,还将提供一个访问器 PyObject * PyType_GetModule(PyTypeObject *)
。如果设置了类型关联的模块,它将返回该模块;否则,它将设置 TypeError
并返回 NULL。如果提供的是静态类型,它将始终设置 TypeError
并返回 NULL。
为了在 CPython 中实现此功能,PyHeapTypeObject
结构将获得一个新成员 PyObject *ht_module
,用于存储指向关联模块的指针。默认情况下它为 NULL
,并且在创建类型对象后不应修改。
子类不会继承 ht_module
成员;需要使用 PyType_FromSpecWithBases
为每个需要它的单个类型设置此成员。
通常,使用设置了 ht_module
的类创建类会导致涉及类和模块的引用循环。这不是问题,因为拆卸模块不是性能敏感的操作,并且模块级函数通常也会创建引用循环。现有的将所有模块全局变量设置为 None 的代码会通过 f_globals
打破函数循环,也会通过 ht_module
打破新循环。
将定义类传递给扩展方法
将为 PyMethodDef.ml_flags
添加一个新的签名标志 METH_METHOD
。从概念上讲,它将 defining_class
添加到函数签名中。为了简化初始实现,该标志只能用作 (METH_FASTCALL | METH_KEYWORDS | METH_METHOD)
。(它不能与其他标志一起使用,例如 METH_O
或裸 METH_FASTCALL
,但可以与 METH_CLASS
或 METH_STATIC
组合使用)。
使用此标志组合定义的方法的 C 函数将使用名为 PyCMethod
的新的 C 签名调用。
PyObject *PyCMethod(PyObject *self,
PyTypeObject *defining_class,
PyObject *const *args,
size_t nargsf,
PyObject *kwnames)
将来(甚至在该 PEP 的初始实现中)可能会添加 (METH_VARARGS | METH_METHOD)
之类的其他组合。但是,METH_METHOD
始终应该是附加标志,即,只有在需要时才会传递定义类。
在 CPython 中,将添加一个扩展 PyCFunctionObject
的新结构来保存额外信息。
typedef struct {
PyCFunctionObject func;
PyTypeObject *mm_class; /* Passed as 'defining_class' arg to the C func */
} PyCMethodObject;
当 PyCFunction
实现发现设置了 METH_METHOD
标志时,它会将 mm_class
传递到 PyCMethod
C 函数中。将添加一个新的宏 PyCFunction_GET_CLASS(cls)
来更方便地访问 mm_class
。
如果 C 方法不需要访问其定义类/模块,它们可以继续使用其他 METH_*
签名。如果未设置 METH_METHOD
,则将类型强制转换为 PyCMethodObject
是无效的。
参数诊所
为了支持将定义类传递给使用 Argument Clinic 的方法,将在 CPython 的 Argument Clinic 工具中添加一个名为 defining_class
的新转换器。
每个方法只能有一个使用此转换器的参数,并且该参数必须出现在 self
之后,或者如果未使用 self
,则作为第一个参数。该参数的类型为 PyTypeObject *
。
使用时,Argument Clinic 会选择 METH_FASTCALL | METH_KEYWORDS | METH_METHOD
作为调用约定。该参数不会出现在 __text_signature__
中。
新的转换器最初将不兼容 __init__
和 __new__
方法,这些方法不能使用 METH_METHOD
约定。
辅助函数
从堆类型获取 每个模块状态 是一个非常常见的任务。为了简化此操作,将添加一个助手。
void *PyType_GetModuleState(PyObject *type)
此函数接受一个堆类型,如果成功,它将返回指向堆类型所属模块状态的指针。
如果失败,可能会发生两种情况。如果传入的是非类型对象或没有模块的类型,则会设置 TypeError
并返回 NULL
。如果找到模块,则会返回指向状态的指针(该指针可能是 NULL
),而不会设置任何异常。
初始实现中转换的模块
为了验证该方法,在初始实现过程中将修改 _elementtree
模块。
API 更改和添加的摘要
以下将添加到 Python C-API。
PyType_FromModuleAndSpec
函数PyType_GetModule
函数PyType_GetModuleState
函数METH_METHOD
调用标志PyCMethod
函数签名
以下添加内容将作为 CPython 实现细节添加,不会进行记录。
PyCFunction_GET_CLASS
宏PyCMethodObject
结构ht_module
成员,属于_heaptypeobject
- Argument Clinic 中的
defining_class
转换器
向后兼容性
在所有堆类型中添加了一个新的指针。所有其他更改都是添加新函数和结构,或者对私有实现细节进行更改。
实现
可能的未来扩展
槽方法
将来可能会添加一种将定义类(或模块状态)传递给槽位方法的方法。
此 PEP 的先前版本建议使用一个辅助函数,该函数将通过搜索 MRO 来确定定义类,该 MRO 包含定义特定函数的槽位的类。但是,如果类发生变异(对于堆类型,这可能来自 Python 代码),则此方法将失败。解决此问题的方案将留待以后讨论。
轻松创建具有模块引用的类型
可以添加一个 PEP 489 执行槽位类型,使创建堆类型的过程比调用 PyType_FromModuleAndSpec
容易得多。这将留待未来的 PEP 进行讨论。
添加一个从有限 API 创建静态异常类型的好方法可能很有用。此类异常类型可以在子解释器之间共享,但实例化时不需要特定的模块状态。这也将留待以后讨论。
优化
如这里所述,使用 METH_METHOD
标志定义的方法只支持一种特定签名。
如果事实证明出于性能原因需要其他签名,则可以添加这些签名。
参考
版权
本文档放置在公共领域或根据 CC0-1.0-Universal 许可,以较宽松的许可为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0573.rst
最后修改时间:2023-10-11 12:05:51 GMT