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,该 PEP 提出了一个更简单的可调用对象公共 C API。
摘要
提出了一种新的“C 调用”协议。它适用于需要实现快速调用的表示函数或方法的类。目标是将内置函数的所有现有优化推广到任意扩展类型。
在参考实现中,此新协议用于现有类 builtin_function_or_method 和 method_descriptor。然而,将来,更多类可能会实现它。
注意:此 PEP 仅处理 Python/C API,不影响 Python 语言或标准库。
动机
标准函数/方法类 builtin_function_or_method 和 method_descriptor 允许非常高效地调用 C 代码。然而,它们不可子类化,这使得它们不适用于许多应用程序:例如,它们提供有限的自省支持(签名仅使用 __text_signature__,没有任意 __qualname__,没有 inspect.getfile())。也不可能存储额外数据以实现 functools.partial 或 functools.lru_cache 之类的功能。因此,用户有许多理由希望在 C 中实现自定义函数/方法类(以鸭子类型的方式)。不幸的是,此类自定义类必然比标准 CPython 函数类慢:字节码解释器具有各种针对 builtin_function_or_method、method_descriptor、method 和 function 实例的优化。
此 PEP 还允许简化现有代码:对 builtin_function_or_method 和 method_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_dictoffset 和 tp_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 的一部分。因此,将 tp_print 字段从 printfunc(函数指针)更改为 Py_ssize_t 不应该是一个问题,即使这会更改 PyTypeObject 结构的内存布局。此外,在所有通常构建二进制文件的系统(Windows、Linux、macOS)上,printfunc 和 Py_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_VARARGS:cc_func(PyObject *self, PyObject *args)CCALL_VARARGS | CCALL_KEYWORDS:cc_func(PyObject *self, PyObject *args, PyObject *kwds)(kwds是NULL或字典;此字典不得被被调用者修改)CCALL_FASTCALL:cc_func(PyObject *self, PyObject *const *args, Py_ssize_t nargs)CCALL_FASTCALL | CCALL_KEYWORDS:cc_func(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)(kwnames是NULL或非空关键字名称元组)CCALL_NOARGS:cc_func(PyObject *self, PyObject *unused)(第二个参数始终为NULL)CCALL_O:cc_func(PyObject *self, PyObject *arg)
标志 CCALL_DEFARG 可以与其中任何一个组合。如果组合,C 函数将接受一个额外的参数作为 self 之前的第一个参数,即指向用于此调用的 PyCCallDef 结构的 const 指针。例如,我们有以下签名
CCALL_DEFARG | CCALL_VARARGS:cc_func(const PyCCallDef *def, PyObject *self, PyObject *args)
一个例外是 CCALL_DEFARG | CCALL_NOARGS:unused 参数被删除,因此签名变为
CCALL_DEFARG | CCALL_NOARGS:cc_func(const PyCCallDef *def, PyObject *self)
注意:与现有的 METH_... 标志不同,CCALL_... 常量不一定代表单个位。因此,检查 if (cc_flags & CCALL_VARARGS) 并非检查签名的有效方式。这些标志在 Python 版本之间也没有二进制兼容性保证。这允许实现选择最高效的标志数值。在参考实现中,cc_flags & CCALL_SIGNATURE 的合法值恰好构成区间 [0, ..., 11]。这意味着编译器可以轻松优化这些情况下的 switch 语句,使用计算的 goto。
检查 __objclass__
如果设置了 CCALL_OBJCLASS 标志,并且 cr_self 为 NULL(对于扩展类型的未绑定方法就是这种情况),则进行类型检查:函数必须至少带一个位置参数,并且第一个参数(通常称为 self)必须是 cc_parent 的实例(cc_parent 必须是一个类)。否则,将引发 TypeError。
自身切片
如果 cr_self 不为 NULL,或者 cc_flags 中未设置 CCALL_SELFARG 标志,则作为 self 传递的参数就是 cr_self。
如果 cr_self 为 NULL 且设置了 CCALL_SELFARG 标志,则第一个位置参数将从 args 中删除,并作为 self 参数传递给 C 函数。实际上,第一个位置参数被视为 __self__。如果没有位置参数,则会引发 TypeError。
这个过程被称为“自身切片”,如果 cr_self 为 NULL 并且设置了 CCALL_SELFARG,则称函数具有自身切片。
请注意,具有自身切片的 CCALL_NOARGS 函数实际上有一个参数,即 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__。这与 自身切片无关: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): 调用func,使用位置参数args和关键字参数kwds(kwds可以为 NULL)。此函数旨在放入tp_call槽中。PyObject *PyCCall_FastCall(PyObject *func, PyObject *const *args, Py_ssize_t nargs, PyObject *kwds): 调用func,使用args[0]、…、args[nargs-1]给出的nargs个位置参数。参数kwds可以为 NULL(无关键字参数),一个包含name:value项的字典,或者一个包含关键字名称的元组。在后一种情况下,关键字值存储在args数组中,从args[nargs]开始。
访问 PyCCallRoot 和 PyCCallDef 结构的宏
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的简写。
通用 getter,旨在放入 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__属性。
分析
分析事件 c_call、c_return 和 c_exception 仅在调用 builtin_function_or_method 或 method_descriptor 的实际实例时生成。这是为了简化和向后兼容(以便配置文件函数不会接收到它无法识别的对象)。在未来的 PEP 中,我们可能会将 C 级分析扩展到实现 C 调用协议的任意类。
内置函数和方法的更改
此 PEP 的参考实现更改了现有类 builtin_function_or_method 和 method_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和类cls的新对象。PyMethodDef结构的条目用于构造新对象,但不存储指向PyMethodDef结构的指针。C 调用协议的标志根据ml->ml_flags、self和parent自动确定。
现有函数 PyCFunction_New、PyCFunction_NewEx 和 PyDescr_NewMethod 都是通过 PyCFunction_ClsNew 实现的。
未文档化的函数 PyCFunction_GetFlags 和 PyCFunction_GET_FLAGS 已弃用。它们仍然通过将原始 METH_... 标志存储在 cc_flags 中的位域中来人为支持。尽管 PyCFunction_GetFlags 在技术上是 稳定 ABI 的一部分,但它极不可能以这种方式使用:首先,它甚至没有文档。其次,标志 METH_FASTCALL 不是稳定 ABI 的一部分,但它非常常见(因为 Argument Clinic)。因此,如果不能支持 METH_FASTCALL,很难想象 PyCFunction_GetFlags 的用例。事实上,PyCFunction_GET_FLAGS 和 PyCFunction_GetFlags 在 CPython 的 Objects/call.c 之外根本没有使用,这进一步表明这些函数不是特别有用。
继承
扩展类型从基类继承类型标志 Py_TPFLAGS_HAVE_CCALL 和值 tp_ccalloffset,前提是它们以与基类相同的方式实现 tp_call 和 tp_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 没有任何区别(即所有函数都保持相同的支持功能)。
唯一潜在的破坏是那些访问 PyCFunctionObject 和 PyMethodDescrObject 内部的 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 *。这有各种用例
- 被调用者可以使用
cc_parent字段,这对于 PEP 573 非常有用。 - 应用程序可以自由地用用户定义字段扩展
PyCCallDef结构,然后可以类似地访问这些字段。 - 在
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 相同问题的另一种方法。有关 PEP 576 和 PEP 580 之间差异的评论,请参阅 https://mail.python.org/pipermail/python-dev/2018-July/154238.html。
讨论
有关此 PEP 在 python-dev 邮件列表中讨论的线程链接
- https://mail.python.org/pipermail/python-dev/2018-June/153938.html
- https://mail.python.org/pipermail/python-dev/2018-June/153984.html
- https://mail.python.org/pipermail/python-dev/2018-July/154238.html
- https://mail.python.org/pipermail/python-dev/2018-July/154470.html
- https://mail.python.org/pipermail/python-dev/2018-July/154571.html
- https://mail.python.org/pipermail/python-dev/2018-September/155166.html
- https://mail.python.org/pipermail/python-dev/2018-October/155403.html
- https://mail.python.org/pipermail/python-dev/2019-March/156853.html
- https://mail.python.org/pipermail/python-dev/2019-March/156879.html
参考实现
参考实现可在 https://github.com/jdemeyer/cpython/tree/pep580 找到
有关使用 C 调用协议的示例,以下分支使用 PEP 580 实现了 functools.lru_cache:https://github.com/jdemeyer/cpython/tree/lru580
参考资料
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0580.rst