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

Python 增强提案

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 3121PEP 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_CLASSMETH_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 转换器

向后兼容性

在所有堆类型中添加了一个新的指针。所有其他更改都是添加新函数和结构,或者对私有实现细节进行更改。

实现

初始实现可以在 Github 存储库中找到 [3];补丁集位于 [4]

可能的未来扩展

槽方法

将来可能会添加一种将定义类(或模块状态)传递给槽位方法的方法。

此 PEP 的先前版本建议使用一个辅助函数,该函数将通过搜索 MRO 来确定定义类,该 MRO 包含定义特定函数的槽位的类。但是,如果类发生变异(对于堆类型,这可能来自 Python 代码),则此方法将失败。解决此问题的方案将留待以后讨论。

轻松创建具有模块引用的类型

可以添加一个 PEP 489 执行槽位类型,使创建堆类型的过程比调用 PyType_FromModuleAndSpec 容易得多。这将留待未来的 PEP 进行讨论。

添加一个从有限 API 创建静态异常类型的好方法可能很有用。此类异常类型可以在子解释器之间共享,但实例化时不需要特定的模块状态。这也将留待以后讨论。

优化

如这里所述,使用 METH_METHOD 标志定义的方法只支持一种特定签名。

如果事实证明出于性能原因需要其他签名,则可以添加这些签名。

参考


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

最后修改时间:2023-10-11 12:05:51 GMT