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,该 PEP 提出了一个更简单的可调用对象公共 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_CCALLtp_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)上,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_VARARGS: cc_func(PyObject *self, PyObject *args)
  • CCALL_VARARGS | CCALL_KEYWORDS: cc_func(PyObject *self, PyObject *args, PyObject *kwds)kwdsNULL 或字典;此字典不得被被调用者修改)
  • 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)kwnamesNULL 或非空关键字名称元组)
  • 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_NOARGSunused 参数被删除,因此签名变为

  • 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 和关键字参数 kwdskwds 可以为 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] 开始。

访问 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 的简写。

通用 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_callc_returnc_exception 仅在调用 builtin_function_or_methodmethod_descriptor 的实际实例时生成。这是为了简化和向后兼容(以便配置文件函数不会接收到它无法识别的对象)。在未来的 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 和类 cls 的新对象。PyMethodDef 结构的条目用于构造新对象,但不存储指向 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 相同问题的另一种方法。有关 PEP 576PEP 580 之间差异的评论,请参阅 https://mail.python.org/pipermail/python-dev/2018-July/154238.html

讨论

有关此 PEP 在 python-dev 邮件列表中讨论的线程链接

参考实现

参考实现可在 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

最后修改:2025-02-01 08:55:40 GMT