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年6月2日
Python 版本:
3.9
发布历史:


目录

摘要

本 PEP 提议为 CPython 扩展方法添加一种访问上下文的方式,例如它们所定义模块的状态。

这将允许扩展方法使用直接指针解引用而不是 PyState_FindModule 来查找模块状态,从而减少或消除使用模块作用域状态而非进程全局状态的性能开销。

这解决了采用 PEP 3121(扩展模块初始化和终结)和 PEP 489(多阶段扩展模块初始化)的剩余障碍之一。

尽管本 PEP 朝着完全解决 PEP 3121PEP 489 开始解决的问题又迈出了一步,但它并不试图解决所有剩余问题。特别是,从槽方法(nb_add 等)访问模块状态的问题尚未解决。

术语

进程全局状态

C 级静态变量。由于这是非常低级的内存存储,因此必须仔细管理。

模块级状态

模块对象本地的状态,作为模块对象初始化的一部分动态分配。这使状态与模块的其他实例(包括其他子解释器中的实例)隔离。

通过 PyModule_GetState() 访问。

静态类型

定义为 C 级静态变量的类型对象,即编译入的类型对象。

静态类型需要在模块实例之间共享,并且不包含其所属模块的信息。静态类型没有 __dict__(尽管其实例可能有)。

堆类型

在运行时创建的类型对象。

定义类

方法的定义类(无论是绑定还是非绑定)是定义该方法的类。仅仅从其基类继承方法的类不是定义类。

例如,intTrue.to_bytesTrue.__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 的最大障碍是允许从扩展类型的方法访问模块状态。目前,从扩展方法访问此状态的方法是通过 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__ 进行基于名称的查找:要么是访问定义类和 Python 函数对象的 Foo 属性,要么是查找 sys.modules 中模块对象的 __name__

在 Python 代码中,这是可行的,因为在执行函数定义时,__globals__ 会适当设置,即使命名空间已被操作以返回不同的对象,最坏的情况也会引发异常。

__class__ 闭包 (2) 是一种更安全的获取定义类的方式,但它仍然依赖于 __closure__ 的适当设置。

相比之下,扩展方法通常作为普通 C 函数实现。这意味着它们只能访问其参数以及 C 级线程局部和进程全局状态。传统上,许多扩展模块将其共享状态存储在 C 级进程全局变量中,这在以下情况下会引发问题

  • 在同一进程中运行多个初始化/终结周期
  • 重新加载模块(例如,测试条件导入)
  • 在子解释器中加载扩展模块

PEP 3121 试图通过提供 PyState_FindModule API 来解决此问题,但对于扩展方法(而非模块级函数)而言,这仍然存在重大问题

  • 它明显慢于直接访问 C 级进程全局状态
  • 它仍然固有地依赖于进程全局状态,这意味着它仍然不能可靠地处理模块重新加载

还有一种情况是,在查找 C 级结构(如模块状态)时,提供意外的对象布局可能会使解释器崩溃,因此确保扩展方法接收到它们期望的对象类型更为重要。

提案

目前,绑定扩展方法(PyCFunctionPyCFunctionWithKeywords)只接收 self,以及(如果适用)提供的位置和关键字参数。

虽然模块级扩展函数已经通过它们的 self 参数获得了对定义模块对象的访问权限,但扩展类型的方法却没有这种便利:它们通过 self 接收绑定实例,因此无法直接访问定义类或模块级状态。

上述附加模块级上下文可以通过两个更改来实现。这两个新增功能都是可选的;扩展作者需要选择启用才能开始使用它们

  • 向堆类型对象添加指向模块的指针。
  • 将定义类传递给底层 C 函数。

    在 CPython 中,定义类在创建内置方法对象 (PyCFunctionObject) 时很容易获得,因此可以将其存储在一个扩展 PyCFunctionObject 的新结构中。

然后可以通过 PyModule_GetState 从模块对象检索模块状态。

请注意,此提议意味着任何需要访问模块级状态的类型都必须是堆类型,而不是静态类型。这对于支持从单个扩展加载多个模块对象是必要的:静态类型作为 C 级全局变量,不包含其所属模块对象的任何信息。

槽方法

上述更改不涵盖槽方法,例如 tp_iternb_add

槽方法的问题在于它们的 C API 是固定的,因此我们不能简单地添加一个新参数来传入定义类。对此问题提出了两种可能的解决方案

  • 通过遍历 MRO 查找类。这可能开销很大,但如果性能不是问题(例如在引发模块级异常时)则可以使用。
  • 将每个槽的定义类指针存储在单独的表 __typeslots__ [2] 中。这在技术上是可行的且快速,但侵入性很大。

受此问题影响的模块还可以选择使用线程局部状态PEP 567 上下文变量作为缓存机制,或者定义自己的重新加载友好查找缓存方案。

普遍解决此问题将推迟到未来的 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_FromModuleAndSpec 来设置它。

通常,创建设置了 ht_module 的类会创建涉及类和模块的引用循环。这不是问题,因为模块的拆解不是性能敏感的操作,并且模块级函数通常也会创建引用循环。通过 f_globals 打破函数循环的现有“将所有模块全局变量设置为 None”代码也将打破通过 ht_module 形成的新循环。

将定义类传递给扩展方法

将添加一个新的签名标志 METH_METHOD,用于 PyMethodDef.ml_flags。从概念上讲,它将 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 结构
  • _heaptypeobjectht_module 成员
  • Argument Clinic 中的 defining_class 转换器

向后兼容性

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

实施

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

未来可能的扩展

槽方法

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

此 PEP 的先前版本提出了一个辅助函数,该函数将通过在 MRO 中搜索定义了特定函数槽的类来确定定义类。但是,如果类被修改(对于堆类型,这在 Python 代码中是可能的),此方法将失败。解决此问题留待未来的讨论。

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

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

最好能有一种好方法通过受限 API 创建静态异常类型。这样的异常类型可以在子解释器之间共享,但无需特定模块状态即可实例化。这也留待未来可能的讨论。

优化

如本文所述,使用 METH_METHOD 标志定义的方法仅支持一种特定签名。

如果出于性能原因需要其他签名,可以添加。

参考资料


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

最后修改:2025-02-01 08:59:27 GMT