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

Python 增强提案

PEP 580 – C 调用协议

作者:
Jeroen Demeyer <J.Demeyer at UGent.be>
BDFL 代表:
Petr Viktorin
状态:
已拒绝
类型:
标准跟踪
创建:
2018年6月14日
Python 版本:
3.8
历史记录:
2018年6月20日,2018年6月22日,2018年7月16日

目录

拒绝通知

此 PEP 被拒绝,转而支持 PEP 590,该提案为可调用对象提供了一个更简单的公共 C API。

摘要

提出了一种新的“C 调用”协议。它适用于表示需要实现快速调用的函数或方法的类。目标是将所有现有的内置函数优化推广到任意扩展类型。

在参考实现中,此新协议用于现有的类 builtin_function_or_methodmethod_descriptor。但是,将来可能会有更多类实现它。

注意:此 PEP 仅处理 Python/C API,它不影响 Python 语言或标准库。

动机

标准函数/方法类 builtin_function_or_methodmethod_descriptor 允许非常高效地调用 C 代码。但是,它们不可子类化,这使得它们不适合许多应用程序:例如,它们提供有限的自省支持(仅使用 __text_signature__ 的签名,没有任意的 __qualname__,没有 inspect.getfile())。也不可能存储其他数据来实现诸如 functools.partialfunctools.lru_cache 之类的东西。因此,用户有很多理由希望在 C 中实现自定义函数/方法类(从鸭子类型意义上讲)。不幸的是,此类自定义类必然比标准 CPython 函数类慢:字节码解释器具有各种针对 builtin_function_or_methodmethod_descriptormethodfunction 实例的特定优化。

此 PEP 还允许简化现有代码:检查 builtin_function_or_methodmethod_descriptor 可以简单地替换为检查和使用 C 调用协议。未来的 PEP 可能会为更多类实现 C 调用协议,从而实现进一步的简化。

我们还设计了 C 调用协议,以便将来可以轻松地扩展新功能。

有关更多背景和动机,请参阅 PEP 579

概述

当前,CPython 对几个特定函数类进行了多种快速调用的优化。一个很好的例子是操作码 CALL_FUNCTION 的实现,其结构如下(查看实际代码

if (PyCFunction_Check(func)) {
    return _PyCFunction_FastCallKeywords(func, stack, nargs, kwnames);
}
else if (Py_TYPE(func) == &PyMethodDescr_Type) {
    return _PyMethodDescr_FastCallKeywords(func, stack, nargs, kwnames);
}
else {
    if (PyMethod_Check(func) && PyMethod_GET_SELF(func) != NULL) {
        /* ... */
    }
    if (PyFunction_Check(func)) {
        return _PyFunction_FastCallKeywords(func, stack, nargs, kwnames);
    }
    else {
        return _PyObject_FastCallKeywords(func, stack, nargs, kwnames);
    }
}

使用 tp_call 槽调用这些特殊情况类的实例比使用优化慢。此 PEP 的基本思想是为用户 C 代码启用此类优化,作为调用方和被调用方。

现有的类 builtin_function_or_method 和其他一些类使用 PyMethodDef 结构来描述底层 C 函数及其签名。第一个具体更改是用新的结构 PyCCallDef 替换它。它存储与 PyMethodDef 相同的一些信息,但有一个重要的补充:函数的“父对象”(定义它的类或模块)。请注意,PyMethodDef 数组仍用于构造函数/方法,但不再用于调用它们。

其次,我们希望每个类都可以使用此类 PyCCallDef 来优化调用,因此 PyTypeObject 结构获得了一个 tp_ccalloffset 字段,该字段给出对象结构中 PyCCallDef * 的偏移量,以及一个标志 Py_TPFLAGS_HAVE_CCALL,指示 tp_ccalloffset 有效。

第三,由于我们希望有效地处理未绑定和绑定的方法(而不是仅处理普通函数),因此我们需要在协议中处理 __self__:在对象结构中的 PyCCallDef * 之后,有一个 PyObject *self 字段。这两个字段一起被称为 PyCCallRoot 结构。

使用这些新结构高效调用对象的新协议称为“C 调用协议”。

注意:在此 PEP 中,“未绑定方法”和“绑定方法”指的是通用行为,而不是特定类。例如,在应用 __get__ 后,未绑定方法将变成绑定方法。

新的数据结构

PyTypeObject 结构获得了一个新的字段 Py_ssize_t tp_ccalloffset 和一个新的标志 Py_TPFLAGS_HAVE_CCALL。如果设置了此标志,则假定 tp_ccalloffset 是对象结构内的一个有效偏移量(类似于 tp_dictoffsettp_weaklistoffset)。它必须是一个严格的正整数。在该偏移量处,会出现一个 PyCCallRoot 结构

typedef struct {
    const PyCCallDef *cr_ccall;
    PyObject         *cr_self;  /* __self__ argument for methods */
} PyCCallRoot;

PyCCallDef 结构包含描述如何调用函数所需的一切

typedef struct {
    uint32_t  cc_flags;
    PyCFunc   cc_func;    /* C function to call */
    PyObject *cc_parent;  /* class or module */
} PyCCallDef;

__self__ 放置在 PyCCallDef 之外的原因是 PyCCallDef 不打算在创建函数后更改。单个 PyCCallDef 可以由未绑定方法和多个绑定方法共享。如果我们将 __self__ 放入该结构中,这将无法实现。

注意:与 tp_dictoffset 不同,我们不允许 tp_ccalloffset 为负数表示从末尾开始计数。似乎没有使用案例,这只会使实现复杂化。

父对象

cc_parent 字段(例如通过 Python 代码中的 __parent____objclass__ 描述符访问)可以是任何 Python 对象或 NULL。自定义类可以自由地将 cc_parent 设置为它们想要的任何值。只有在设置了 CCALL_OBJCLASS 标志时,C 调用协议才会使用它。

对于扩展类型的成员方法,cc_parent 指向定义该方法的类(可能是 type(self) 的超类)。目前从方法的代码中检索它并非易事。将来,这可用于通过定义类访问模块状态。有关详细信息,请参阅 PEP 573 的基本原理。

当设置了 CCALL_OBJCLASS 标志(对于扩展类型的成员方法将设置此标志)时,cc_parent 用于执行以下类型的检查

>>> list.append({}, "x")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: descriptor 'append' requires a 'list' object but received a 'dict'

对于模块的函数,cc_parent 设置为模块。目前,这与 __self__ 完全相同。但是,将 __self__ 用于模块是当前实现的一个特性:将来,我们希望允许以正常方式使用 __self__ 来实现方法的函数。此类函数仍然可以使用 cc_parent 来代替引用模块。

父对象通常还用于实现 __qualname__。新的 C API 函数 PyCCall_GenericGetQualname() 正是这样做的。

使用 tp_print

我们建议用 tp_ccalloffset 替换现有的未使用字段 tp_print。由于 Py_TPFLAGS_HAVE_CCALL 不会添加到 Py_TPFLAGS_DEFAULT 中,因此可以确保对于设置 tp_print 的现有扩展模块完全向后兼容。这也意味着当指定了 Py_TPFLAGS_HAVE_CCALL 时,我们可以要求 tp_ccalloffset 是一个有效的偏移量:我们不需要检查 tp_ccalloffset != 0。在未来的 Python 版本中,我们可能会决定 tp_print 无条件地变为 tp_ccalloffset,删除 Py_TPFLAGS_HAVE_CCALL 标志,而是检查 tp_ccalloffset != 0

注意PyTypeObject 的确切布局不是稳定 ABI 的一部分(参见 PEP 384 – 定义稳定 ABI)。因此,将 tp_print 字段从 printfunc(一个函数指针)更改为 Py_ssize_t 应该不会有问题,即使这改变了 PyTypeObject 结构的内存布局。此外,在所有通常构建二进制文件的系统(Windows、Linux、macOS)上,printfuncPy_ssize_t 的大小相同,因此二进制兼容性问题根本不会出现。

C 调用协议

我们说一个类实现了 C 调用协议,如果它设置了 Py_TPFLAGS_HAVE_CCALL 标志(如上所述,它必须随后设置 tp_ccalloffset > 0)。这样的类必须实现 __call__,如本节所述(在实践中,这仅仅意味着将 tp_call 设置为 PyCCall_Call)。

cc_func 字段是一个 C 函数指针,它与 PyMethodDef 的现有 ml_meth 字段的作用相同。它的精确签名取决于标志。影响 cc_func 签名的标志子集由位掩码 CCALL_SIGNATURE 给出。以下是 cc_flags & CCALL_SIGNATURE 的可能值以及 C 函数所接受的参数。返回值始终为 PyObject *。以下是现有 PyMethodDef 签名标志的类似物

  • CCALL_VARARGScc_func(PyObject *self, PyObject *args)
  • CCALL_VARARGS | CCALL_KEYWORDScc_func(PyObject *self, PyObject *args, PyObject *kwds)kwdsNULL 或字典;此字典不得被被调用者修改)
  • CCALL_FASTCALLcc_func(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
  • CCALL_FASTCALL | CCALL_KEYWORDScc_func(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)kwnamesNULL 或关键字名称的非空元组)
  • CCALL_NOARGScc_func(PyObject *self, PyObject *unused)(第二个参数始终为 NULL
  • CCALL_Occ_func(PyObject *self, PyObject *arg)

标志 CCALL_DEFARG 可以与任何这些组合。如果是这样,C 函数会在 self 之前作为第一个参数接受一个额外的参数,即指向用于此调用的 PyCCallDef 结构的常量指针。例如,我们有以下签名

  • CCALL_DEFARG | CCALL_VARARGScc_func(const PyCCallDef *def, PyObject *self, PyObject *args)

一个例外是 CCALL_DEFARG | CCALL_NOARGSunused 参数被丢弃,因此签名变为

  • CCALL_DEFARG | CCALL_NOARGScc_func(const PyCCallDef *def, PyObject *self)

注意:与现有的 METH_... 标志不同,CCALL_... 常量不一定表示单个位。因此,检查 if (cc_flags & CCALL_VARARGS) 不是检查签名的有效方法。这些标志在 Python 版本之间也没有二进制兼容性的保证。这允许实现选择标志最有效的数值。在参考实现中,cc_flags & CCALL_SIGNATURE 的合法值恰好构成区间 [0,…,11]。这意味着编译器可以使用计算 goto 轻松优化这些情况下的 switch 语句。

检查 __objclass__

如果设置了 CCALL_OBJCLASS 标志并且 cr_self 为 NULL(对于扩展类型的未绑定方法就是这种情况),则会执行类型检查:函数必须至少用一个位置参数调用,并且第一个(通常称为 self)必须是 cc_parent(它必须是一个类)的实例。如果不是,则会引发 TypeError

自身切片

如果 cr_self 不为 NULL 或 cc_flags 中未设置标志 CCALL_SELFARG,则作为 self 传递的参数只是 cr_self

如果 cr_self 为 NULL 且设置了标志 CCALL_SELFARG,则从 args 中删除第一个位置参数,并将其作为 self 参数传递给 C 函数。实际上,第一个位置参数被视为 __self__。如果没有位置参数,则会引发 TypeError

此过程称为“self 切片”,如果 cr_self 为 NULL 且设置了 CCALL_SELFARG,则称函数具有 self 切片。

请注意,具有 self 切片的 CCALL_NOARGS 函数实际上有一个参数,即 self。类似地,具有 self 切片的 CCALL_O 函数有两个参数。

描述符行为

支持 C 调用协议的类必须以特定方式实现描述符协议。

这是为了有效地实现绑定方法:如果其他代码可以对 __get__ 的行为做出假设,则可以实现否则不可能实现的优化。特别是,我们希望允许在绑定方法和未绑定方法之间共享 PyCCallDef 结构。我们还需要正确实现 _PyObject_GetMethod,它由 LOAD_METHOD/CALL_METHOD 优化使用。

首先,如果 func 支持 C 调用协议,则不得实现 func.__set__func.__delete__

其次,func.__get__ 必须按如下方式执行

  • 如果 cr_self 不为 NULL,则 __get__ 必须是无操作的,这意味着 func.__get__(obj, cls)(*args, **kwds) 的行为与 func(*args, **kwds) 完全相同。也可以不实现 __get__
  • 如果 cr_self 为 NULL,则 func.__get__(obj, cls)(*args, **kwds)(其中 obj 不为 None)必须等效于 func(obj, *args, **kwds)。特别是,在这种情况下必须实现 __get__。这与 self 切片 无关:obj 可以作为 self 参数传递给 C 函数,也可以作为第一个位置参数传递。
  • 如果 cr_self 为 NULL,则 func.__get__(None, cls)(*args, **kwds) 必须等效于 func(*args, **kwds)

对象 func.__get__(obj, cls) 没有限制。例如,后者不需要实现 C 调用协议。我们只指定 func.__get__(obj, cls).__call__ 的作用。

对于完全不关心 __self____get__ 的类,最简单的解决方案是赋值 cr_self = Py_None(或任何其他非 NULL 值)。

__name__ 属性

C 调用协议要求该函数具有一个 __name__ 属性,该属性的类型为 str(而不是子类)。

此外,由__name__返回的对象必须存储在某个地方;它不能是一个临时对象。这是必需的,因为PyEval_GetFuncName使用对__name__属性的借用引用(另请参见[2])。

通用 API 函数

本节列出了处理 C 调用协议的新公共 API 函数或宏。

  • int PyCCall_Check(PyObject *op):如果op实现了 C 调用协议,则返回 true。

以下所有函数和宏都适用于支持 C 调用协议的任何实例。换句话说,PyCCall_Check(func)必须为 true。

  • PyObject *PyCCall_Call(PyObject *func, PyObject *args, PyObject *kwds):使用位置参数args和关键字参数kwds调用funckwds可以为 NULL)。此函数旨在放置在tp_call槽中。
  • PyObject *PyCCall_FastCall(PyObject *func, PyObject *const *args, Py_ssize_t nargs, PyObject *kwds):使用由args[0]、…、args[nargs-1]给出的nargs个位置参数调用func。参数kwds可以为 NULL(无关键字参数)、一个包含name:value项的字典或一个包含关键字名称的元组。在后一种情况下,关键字值存储在args数组中,从args[nargs]开始。

访问PyCCallRootPyCCallDef结构的宏

  • const PyCCallRoot *PyCCall_CCALLROOT(PyObject *func):指向func内部的PyCCallRoot结构的指针。
  • const PyCCallDef *PyCCall_CCALLDEF(PyObject *func)PyCCall_CCALLROOT(func)->cr_ccall的简写。
  • uint32_t PyCCall_FLAGS(PyObject *func)PyCCall_CCALLROOT(func)->cr_ccall->cc_flags的简写。
  • PyObject *PyCCall_SELF(PyOject *func)PyCCall_CCALLROOT(func)->cr_self的简写。

通用获取器,旨在放入tp_getset数组中

  • PyObject *PyCCall_GenericGetParent(PyObject *func, void *closure):返回cc_parent。如果cc_parent为 NULL,则引发AttributeError
  • PyObject *PyCCall_GenericGetQualname(PyObject *func, void *closure):返回一个适合用作__qualname__的字符串。如果可能,这将使用cc_parent__qualname__。它还使用__name__属性。

性能分析

仅当调用builtin_function_or_methodmethod_descriptor的实际实例时,才会生成概要分析事件c_callc_returnc_exception。这样做是为了简单起见,也为了向后兼容(这样概要分析函数就不会接收它无法识别的对象)。在未来的 PEP 中,我们可能会将 C 级概要分析扩展到实现 C 调用协议的任意类。

对内置函数和方法的更改

此 PEP 的参考实现将现有的类builtin_function_or_methodmethod_descriptor更改为使用 C 调用协议。实际上,这两个类几乎合并了:实现变得非常相似,但它们仍然是单独的类(主要是为了向后兼容)。PyCCallDef结构只是作为对象结构的一部分存储。这两个类都使用PyCFunctionObject作为对象结构。这是这两个类的新的布局

typedef struct {
    PyObject_HEAD
    PyCCallDef  *m_ccall;
    PyObject    *m_self;         /* Passed as 'self' arg to the C function */
    PyCCallDef   _ccalldef;      /* Storage for m_ccall */
    PyObject    *m_name;         /* __name__; str object (not NULL) */
    PyObject    *m_module;       /* __module__; can be anything */
    const char  *m_doc;          /* __text_signature__ and __doc__ */
    PyObject    *m_weakreflist;  /* List of weak references */
} PyCFunctionObject;

对于模块的函数和扩展类型的未绑定方法,m_ccall指向_ccalldef字段。对于绑定方法,m_ccall指向未绑定方法的PyCCallDef

**注意**:method_descriptor的新布局将其更改为不再以PyDescr_COMMON开头。这纯粹是一个实现细节,应该会导致很少(如果有的话)的兼容性问题。

C API 函数

添加了以下函数(也添加到稳定 ABI中)

  • PyObject * PyCFunction_ClsNew(PyTypeObject *cls, PyMethodDef *ml, PyObject *self, PyObject *module, PyObject *parent):创建一个新的对象,其对象结构为PyCFunctionObject,类为clsPyMethodDef结构的条目用于构造新对象,但不会存储指向PyMethodDef结构的指针。C 调用协议的标志根据ml->ml_flagsselfparent自动确定。

现有的函数PyCFunction_NewPyCFunction_NewExPyDescr_NewMethod都是根据PyCFunction_ClsNew实现的。

未记录的函数PyCFunction_GetFlagsPyCFunction_GET_FLAGS已弃用。它们仍然通过将原始METH_...标志存储在cc_flags内部的位字段中来人为地支持。尽管PyCFunction_GetFlags在技术上是稳定 ABI的一部分,但它不太可能以这种方式使用:首先,它甚至没有记录在案。其次,标志METH_FASTCALL不是稳定 ABI 的一部分,但它非常常见(因为 Argument Clinic)。因此,如果无法支持METH_FASTCALL,很难想象PyCFunction_GetFlags的用例。PyCFunction_GET_FLAGSPyCFunction_GetFlags根本没有被 CPython 在Objects/call.c之外使用,这进一步表明这些函数并不是特别有用。

继承

扩展类型继承基类的类型标志Py_TPFLAGS_HAVE_CCALL和值tp_ccalloffset,前提是它们以与基类相同的方式实现tp_calltp_descr_get。堆类型永远不会继承 C 调用协议,因为那是不安全的(堆类型可以动态更改)。

性能

此 PEP 不应影响现有代码的性能(无论是正面的还是负面的)。它旨在允许编写高效的新代码,而不是使现有代码更快。

以下是一些指向python-dev邮件列表的指针,其中讨论了性能改进

稳定的 ABI

函数PyCFunction_ClsNew已添加到稳定 ABI中。

与 C 调用协议相关的任何函数、结构或常量均未添加到稳定 ABI 中。

有两个原因:首先,C 调用协议最有用的功能可能是METH_FASTCALL调用约定。鉴于这甚至不是公共 API 的一部分(另请参见PEP 579,问题 6),将 C 调用协议中的任何其他内容添加到稳定 ABI 中将很奇怪。

其次,我们希望 C 调用协议在将来可扩展。通过不向稳定 ABI 添加任何内容,我们可以在不受限制的情况下做到这一点。

向后兼容性

Python 接口或已记录的 C API 没有任何区别(在所有函数都以相同的功能继续受支持的意义上)。

唯一潜在的破坏是访问PyCFunctionObjectPyMethodDescrObject内部的 C 代码。我们预计由于此原因而产生的问题很少。

基本原理

为什么这比 PEP 575 更好?

关于PEP 575的一个主要抱怨是,它将功能(调用和内省协议)与类层次结构耦合在一起:只有当一个类是base_function的子类时,才能从新特性中受益。对于现有的类来说,这样做可能很困难,因为它们可能对C对象结构的布局有其他约束,这些约束来自现有的基类或实现细节。例如,functools.lru_cache无法按原样实现PEP 575

它还使实现变得复杂,正是因为在实现细节和类层次结构中都需要进行更改。

当前的PEP没有这些问题。

为什么将函数指针存储在实例中?

调用对象所需的信息存储在实例中(在PyCCallDef结构中),而不是类中。这与tp_call槽或早期尝试实现tp_fastcall[1]不同。

主要用例是内置函数和方法。对于这些函数,要调用的C函数确实取决于实例。

请注意,当前协议使支持所有实例都调用相同C函数的情况变得很容易:只需对每个实例使用一个静态的PyCCallDef结构即可。

为什么使用 CCALL_OBJCLASS?

标志CCALL_OBJCLASS旨在支持各种情况下需要检查self参数的类的情况,例如

>>> list.append({}, None)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: append() requires a 'list' object but received a 'dict'

>>> list.__len__({})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: descriptor '__len__' requires a 'list' object but received a 'dict'

>>> float.__dict__["fromhex"](list, "0xff")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: descriptor 'fromhex' for type 'float' doesn't apply to type 'list'

在参考实现中,只有第一个使用了新代码。其他示例表明,此类检查出现在多个位置,因此添加对它们的通用支持是有意义的。

为什么使用 CCALL_SELFARG?

标志CCALL_SELFARG和自切片的概念是支持方法所必需的:C函数不应该关心它是作为非绑定方法还是绑定方法被调用。在这两种情况下,都应该有一个self参数,它只是非绑定方法调用的第一个位置参数。

例如,list.append是一个METH_O方法。调用list.append([], 42)[].append(42)都应该转换为C调用list_append([], 42)

借助提议的C调用协议,我们可以以这样一种方式支持这一点:非绑定方法和绑定方法都共享一个PyCCallDef结构(设置了CCALL_SELFARG标志)。

因此,CCALL_SELFARG有两个优点:调用方法没有额外的间接层,并且构造绑定方法不需要设置PyCCallDef结构。

另一个次要优点是,我们可以使错误调用签名的错误消息在Python方法和内置方法之间更加统一。在以下示例中,Python无法确定方法是接受1个还是2个参数

>>> class List(list):
...     def myappend(self, item):
...         self.append(item)
>>> List().myappend(1, 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: myappend() takes 2 positional arguments but 3 were given
>>> List().append(1, 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: append() takes exactly one argument (2 given)

目前,PyCFunction_Call无法知道用户可见参数的实际数量,因为它无法在运行时区分函数(没有self参数)和绑定方法(有self参数)。CCALL_SELFARG标志使这种差异变得明确。

为什么使用 CCALL_DEFARG?

标志CCALL_DEFARG使被调用者可以访问PyCCallDef *。这有各种用例

  1. 被调用者可以使用cc_parent字段,这对于PEP 573很有用。
  2. 应用程序可以自由地使用用户定义的字段扩展PyCCallDef结构,然后可以类似地访问这些字段。
  3. PyCCallDef结构是对象结构的一部分的情况下(例如,对于PyCFunctionObject为真),可以从PyCCallDef指针中减去适当的偏移量以获取指向定义该PyCCallDef的可调用对象的指针。

此PEP的早期版本定义了一个标志CCALL_FUNCARG而不是CCALL_DEFARG,它将可调用对象传递给被调用者。它有类似的用例,但对于绑定方法存在一些歧义: “可调用对象”应该是绑定方法对象还是方法包装的原始函数?通过传递PyCCallDef *,这种歧义消失了,因为绑定方法使用包装函数的PyCCallDef *

替换 tp_print

我们将tp_print重新用作tp_ccalloffset,因为这使得外部项目更容易将C调用协议移植到早期Python版本。特别是,Cython项目已表示有兴趣这样做(请参见https://mail.python.org/pipermail/python-dev/2018-June/153927.html)。

替代建议

PEP 576是解决与本PEP相同问题的另一种方法。请参见https://mail.python.org/pipermail/python-dev/2018-July/154238.html,了解有关PEP 576PEP 580之间区别的评论。

讨论

指向python-dev邮件列表中讨论此PEP的主题的链接

参考实现

参考实现可以在https://github.com/jdemeyer/cpython/tree/pep580找到

有关使用C调用协议的示例,以下分支使用PEP 580实现了functools.lru_cachehttps://github.com/jdemeyer/cpython/tree/lru580

参考文献


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

上次修改时间:2023-09-09 17:39:29 GMT