Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 增强提案

PEP 575 – 统一函数/方法类

作者:
Jeroen Demeyer <J.Demeyer at UGent.be>
状态:
已撤回
类型:
标准跟踪
创建:
2018-03-27
Python 版本:
3.8
历史记录:
2018-03-31, 2018-04-12, 2018-04-27, 2018-05-05

目录

撤回通知

参见 PEP 580,了解允许快速调用自定义类的更好解决方案。

参见 PEP 579,了解有关本 PEP 中其他问题的一些更广泛的讨论。

摘要

重新组织函数和方法的类层次结构,目标是减少内置函数(在 C 中实现)和 Python 函数之间的差异。主要的是,让内置函数的行为更像 Python 函数,而不会牺牲性能。

引入了新的基类 base_function,各种函数类以及 method(重命名为 bound_method)都继承自它。

我们还允许子类化 Python function 类。

动机

目前,CPython 有两个不同的函数类:第一个是 Python 函数,当你使用 deflambda 定义函数时得到的函数。第二个是内置函数,例如 lenisinstancenumpy.dot。这些是在 C 中实现的。

这两个类是完全独立实现的,并且具有不同的功能。特别是,目前无法在 C 中有效地实现函数(只有内置函数可以做到这一点),同时仍然允许像 inspect.signatureinspect.getsourcefile 这样的自省(只有 Python 函数可以做到这一点)。这对像 Cython [1] 这样的项目来说是一个问题,这些项目希望做到这一点。

在 Cython 中,这是通过发明一个名为 cyfunction 的新函数类来解决的。不幸的是,新的函数类会产生问题:inspect 模块不识别这些函数为函数 [2],并且性能更差(CPython 对调用内置函数有特定的优化)。

第二个动机是更普遍地使内置函数和方法的行为更像 Python 函数和方法。例如,Python 未绑定方法只是函数,但扩展类型的未绑定方法(例如 dict.get)是一个不同的类。Python 类的绑定方法具有 __func__ 属性,扩展类型的绑定方法没有。

第三,本 PEP 允许对函数进行高度定制。function 类变得可以子类化,并且还允许使用自定义函数子类来表示在 C 中实现的函数。在后一种情况下,这可以通过与真正的内置函数相同的性能来完成。所有函数都可以访问函数对象(__call__ 中的 self),为 PEP 573 铺平了道路。

新类

这是函数和方法的新类层次结构

              object
                 |
                 |
          base_function
         /       |     \
        /        |      \
       /         |   defined_function
      /          |        \
cfunction (*)    |         \
                 |       function
                 |
           bound_method (*)

用 (*) 标记的两个类 *不允许* 子类化;其他类则允许。

函数和未绑定方法之间没有区别,而绑定方法是 bound_method 的实例。

base_function

base_function 成为所有函数类型的新的基类。它基于现有的 builtin_function_or_method 类,但具有以下差异和新功能

  1. 它充当一个描述符,实现 __get__,如果 m_selfNULL,则将函数转换为方法。如果 m_self 不为 NULL,那么这是一个无操作:返回现有函数。
  2. 一个新的只读属性 __parent__,在 C 结构中表示为 m_parent。如果此属性存在,它表示定义对象。对于扩展类型的 method,这是定义类(在普通 Python 中为 __class__),对于模块的 function,这是定义模块。通常,它可以是任何 Python 对象。如果 __parent__ 是一个类,它具有特殊语义:在这种情况下,必须使用 self 是该类的实例来调用该函数。最后,__qualname____reduce__ 将使用 __parent__ 作为命名空间(而不是之前的 __self__)。
  3. 一个新的属性 __objclass__,如果 __parent__ 是一个类,则等于 __parent__。否则,访问 __objclass__ 会引发 AttributeError。这旨在与 method_descriptor 向后兼容。
  4. 字段 ml_doc 以及属性 __doc____text_signature__(参见 Argument Clinic)不受支持。
  5. 一个新的标志 METH_PASS_FUNCTION,用于 ml_flags。如果设置了此标志,则存储在 ml_meth 中的 C 函数将使用一个额外的第一个参数调用,该参数等于函数对象。
  6. 一个新的标志 METH_BINDING,用于 ml_flags,它只适用于模块的函数(而不是类的 method)。如果设置了此标志,则 m_self 将设置为 NULL 而不是模块。这允许函数的行为更像 Python 函数,因为它启用了 __get__
  7. 一个新的标志 METH_CALL_UNBOUND 用于禁用 自我切片
  8. 一个新的标志 METH_PYTHON,用于 ml_flags。此标志表示此函数应被视为 Python 函数。理想情况下,应避免使用此标志,因为它违反了鸭子类型哲学。但是,它在某些地方仍然需要,例如 分析

base_function 的目标是它支持所有不同的方式来调用函数和方法,只需一个结构。例如,新的标志 METH_PASS_FUNCTION 将由方法的实现使用。

无法直接创建 base_function 的实例(tp_newNULL)。但是,C 代码手动创建实例是合法的。

这些是相关的 C 结构

PyTypeObject PyBaseFunction_Type;

typedef struct {
    PyObject_HEAD
    PyCFunctionDef *m_ml;     /* Description of the C function to call */
    PyObject *m_self;         /* __self__: anything, can be NULL; readonly */
    PyObject *m_module;       /* __module__: anything (typically str) */
    PyObject *m_parent;       /* __parent__: anything, can be NULL; readonly */
    PyObject *m_weakreflist;  /* List of weak references */
} PyBaseFunctionObject;

typedef struct {
    const char *ml_name;   /* The name of the built-in function/method */
    PyCFunction ml_meth;   /* The C function that implements it */
    int ml_flags;          /* Combination of METH_xxx flags, which mostly
                              describe the args expected by the C func */
} PyCFunctionDef;

子类可以使用额外的字段扩展 PyCFunctionDef

Python 属性 __self__ 返回 m_self,除非设置了 METH_STATIC。在这种情况下或如果 m_selfNULL,那么根本没有 __self__ 属性。因此,在本 PEP 中,我们编写 m_self__self__,它们的含义略有不同。

cfunction

这是旧版 builtin_function_or_method 类的全新版本。名称 cfunction 的选择是为了避免与“内置”一词的混淆,这里的“内置”是指“builtins 模块中的内容”。这个名称也更符合 C API,C API 使用 PyCFunction 前缀。

cfunctionbase_function 的副本,但有一些差异:

  1. m_ml 指向一个 PyMethodDef 结构,它扩展了 PyCFunctionDef,并添加了一个 ml_doc 字段来实现 __doc____text_signature__ 作为只读属性。
    typedef struct {
        const char *ml_name;
        PyCFunction ml_meth;
        int ml_flags;
        const char *ml_doc;
    } PyMethodDef;
    

    请注意,PyMethodDefPython 稳定 ABI 的一部分,它被几乎所有扩展模块使用,因此我们绝对不能更改此结构。

  2. 参数诊所 受支持。
  3. __self__ 始终存在。在 base_function.__self__ 会引发 AttributeError 的情况下,将返回 None

类型对象是 PyTypeObject PyCFunction_Type,我们定义 PyCFunctionObject 作为 PyBaseFunctionObject 的别名(除了 m_ml 的类型)。

defined_function

defined_function 是一个抽象基类,用于指示函数具有内省支持。defined_function 的实例需要支持 Python 函数的所有属性,即 __code____globals____doc____defaults____kwdefaults____closure____annotations__。还有一个 __dict__ 用于支持用户添加的属性。

这些属性都不需要有意义。特别是,__code__ 可能不是一个可用的代码对象,可能只填写了一些字段。本 PEP 并没有规定如何实现各种属性。它们可以是简单的结构体成员,也可以是更复杂的描述符。只需要只读支持,不需要任何属性可写。

defined_function 主要用于自动生成的 C 代码,例如由 Cython [1] 生成的代码。没有 API 可以创建它的实例。

C 结构如下所示:

PyTypeObject PyDefinedFunction_Type;

typedef struct {
    PyBaseFunctionObject base;
    PyObject *func_dict;        /* __dict__: dict or NULL */
} PyDefinedFunctionObject;

TODO:也许可以为 defined_function 找到一个更好的名称。其他提议:inspect_function(任何满足 inspect.isfunction 的内容)、builtout_function(一个构建得更好的函数;玩弄了内置一词)、generic_function(最初的提议,但与 functools.singledispatch 泛型函数冲突)、user_function(由用户定义,而不是 CPython)。

function

此类专为用 Python 实现的函数而设计。与其他函数类型不同,function 的实例可以从 Python 代码创建。这没有变化,因此我们不会在本 PEP 中描述这些细节。

C 结构的布局如下所示:

PyTypeObject PyFunction_Type;

typedef struct {
    PyBaseFunctionObject base;
    PyObject *func_dict;        /* __dict__: dict or NULL */
    PyObject *func_code;        /* __code__: code */
    PyObject *func_globals;     /* __globals__: dict; readonly */
    PyObject *func_name;        /* __name__: string */
    PyObject *func_qualname;    /* __qualname__: string */
    PyObject *func_doc;         /* __doc__: can be anything or NULL */
    PyObject *func_defaults;    /* __defaults__: tuple or NULL */
    PyObject *func_kwdefaults;  /* __kwdefaults__: dict or NULL */
    PyObject *func_closure;     /* __closure__: tuple of cell objects or NULL; readonly */
    PyObject *func_annotations; /* __annotations__: dict or NULL */
    PyCFunctionDef _ml;         /* Storage for base.m_ml */
} PyFunctionObject;

描述符 __name__ 返回 func_name。当设置 __name__ 时,还会用 UTF-8 编码的名称更新 base.m_ml->ml_name

字段 _ml 保留了要由 base.m_ml 使用的空间。

一个 base_function 实例必须设置标志 METH_PYTHON 当且仅当它是一个 function 实例。

当从 codeglobals 构造一个 function 实例时,将创建一个实例,其中 base.m_ml = &_mlbase.m_self = NULL

为了让子类化更容易,我们还添加了一个复制构造函数:如果 ffunction 的实例,那么 types.FunctionType(f) 将复制 f。这方便地允许使用自定义函数类型作为装饰器。

>>> from types import FunctionType
>>> class CustomFunction(FunctionType):
...     pass
>>> @CustomFunction
... def f(x):
...     return x
>>> type(f)
<class '__main__.CustomFunction'>

这也消除了许多 functools.wraps 的用例:包装器可以由 function 的子类替换。

bound_method

bound_method 用于所有绑定方法,无论底层函数的类是什么。它在 base_function 的基础上添加了一个新属性:__func__ 指向该函数。

bound_method 替换了旧的 method 类,该类仅用于绑定为方法的 Python 函数。

这里有一个复杂之处,因为我们希望允许从任意可调用对象构造一个方法。这可能是一个已绑定方法,也可能根本不是 base_function 的实例。因此,在实践中,有两种方法:

  • 对于任意可调用对象,我们使用一个带有 METH_PASS_FUNCTION 标志设置的固定 PyCFunctionDef 结构。
  • 对于绑定 base_function 实例的方法(更确切地说,具有 Py_TPFLAGS_BASEFUNCTION 标志设置)并且具有 自身切片 的方法,我们使用原始函数的 PyCFunctionDef。这样,我们在调用绑定方法时就不会损失任何性能。在这种情况下,__func__ 属性仅用于实现各种属性,而不是用于调用方法。

当从 base_function 构造一个新的方法时,我们检查 self 对象是否是 __objclass__ 的实例(如果指定了类作为父类)并在出现错误时引发 TypeError

C 结构如下所示:

PyTypeObject PyMethod_Type;

typedef struct {
    PyBaseFunctionObject base;
    PyObject *im_func;  /* __func__: function implementing the method; readonly */
} PyMethodObject;

调用 base_function 实例

我们指定了 __call__ 的实现,用于 base_function 的实例。

检查 __objclass__

首先,如果函数的 __parent__ 是一个类(回想一下,__objclass__ 将成为 __parent__ 的别名),则执行类型检查:如果 m_selfNULL(对于扩展类型的未绑定方法就是这种情况),那么函数必须至少使用一个位置参数调用,并且第一个(通常称为 self)必须是 __objclass__ 的实例。如果不是,则引发 TypeError

请注意,绑定方法具有 m_self != NULL,因此不会检查 __objclass__。而是,在构造方法时会检查 __objclass__

标志

为了方便起见,我们定义了一个新常量:METH_CALLFLAGS 组合了来自 PyCFunctionDef.ml_flags 的所有标志,这些标志指定要调用的 C 函数的签名。它等于:

METH_VARARGS | METH_FASTCALL | METH_NOARGS | METH_O | METH_KEYWORDS | METH_PASS_FUNCTION

以上前四个标志中必须且只能设置一个标志,并且只有 METH_VARARGSMETH_FASTCALL 可以与 METH_KEYWORDS 组合。违反这些规则是未定义的行为。

还有一些新标志会影响函数调用,即 METH_PASS_FUNCTIONMETH_CALL_UNBOUND。一些标志已在 [5] 中记录。我们在下面解释其他标志。

自我切片

如果函数具有 m_self == NULL 并且标志 METH_CALL_UNBOUND 未设置,则从 *args 中移除第一个位置参数(如果有),并将其作为第一个参数传递给 C 函数。实际上,第一个位置参数将被视为 __self__。这旨在支持未绑定方法,以便 C 函数无法区分绑定方法调用和未绑定方法调用。这不会以任何方式影响关键字参数。

这个过程称为自身切片,如果 m_self == NULL 并且 METH_CALL_UNBOUND 未设置,则称函数具有自身切片

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

METH_PASS_FUNCTION

如果设置了此标志,则 C 函数将以一个额外的第一个参数调用,即函数本身(base_function 实例)。作为特例,如果函数是 bound_method,则传递方法的底层函数(但不递归:如果 bound_method 包裹了 bound_method,则 __func__ 仅应用一次)。

例如,一个普通的 METH_VARARGS 函数具有签名 (PyObject *self, PyObject *args)。使用 METH_VARARGS | METH_PASS_FUNCTION,这将变为 (PyObject *func, PyObject *self, PyObject *args)

METH_FASTCALL

这是一个现有的但未记录的标志。我们建议正式支持并记录它。

如果设置了标志 METH_FASTCALL 但未设置 METH_KEYWORDS,则 ml_meth 字段的类型为 PyCFunctionFast,它接受参数 (PyObject *self, PyObject *const *args, Py_ssize_t nargs)。这样的函数只接受位置参数,并且这些参数作为长度为 nargs 的普通 C 数组 args 传递。

如果设置了标志 METH_FASTCALL | METH_KEYWORDS,则 ml_meth 字段的类型为 PyCFunctionFastKeywords,它接受参数 (PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)。位置参数作为长度为 nargs 的 C 数组 args 传递。关键字参数的在该数组中紧随其后,从位置 nargs 开始。关键字参数的(名称)作为 kwnames 中的 tuple 传递。例如,假设给出了 3 个位置参数和 2 个关键字参数。那么 args 是一个长度为 3 + 2 = 5 的数组,nargs 等于 3,kwnames 是一个 2 元组。

自动创建内置函数

Python 为扩展类型(使用 PyTypeObject.tp_methods 字段)和模块(使用 PyModuleDef.m_methods 字段)自动生成 cfunction 实例。数组 PyTypeObject.tp_methodsPyModuleDef.m_methods 必须是 PyMethodDef 结构的数组。

扩展类型的未绑定方法

未绑定方法的类型从 method_descriptor 变为 cfunction。作为未绑定方法出现的对象与在类 __dict__ 中出现的对象是同一个对象。Python 自动将 __parent__ 属性设置为定义类。

模块的内置函数

对于模块的函数,__parent__ 将设置为模块。除非给出标志 METH_BINDING,否则 __self__ 也将设置为模块(为了向后兼容)。

一个重要的结果是,默认情况下,这样的函数在用作属性时不会变为方法(base_function.__get__ 只有在 m_selfNULL 时才会这样做)。有人可能会认为这是一个错误,但这样做是为了向后兼容的原因:在一个关于 python-ideas 的早期帖子中[6],大家一致同意保留内置函数的这个缺陷。

但是,为了允许对特定或新实现的内置函数这样做,标志 METH_BINDING 会阻止设置 __self__

进一步的改变

新的类型标志

一个新的 PyTypeObject 标志(用于 tp_flags)被添加:Py_TPFLAGS_BASEFUNCTION 表示此类型的实例是函数,可以像 base_function 一样被调用和绑定为方法。

这不同于像 Py_TPFLAGS_LIST_SUBCLASS 这样的标志,因为它表示的不仅仅是子类:它还表示 __call____get__ 的默认实现。特别是,base_function 的此类子类必须遵循部分 调用 base_function 实例 中的实现。

此标志会自动为从 base_function 继承 tp_calltp_descr_get 实现的扩展类型设置。如果扩展类型以兼容的方式覆盖了 __call____get__,则可以显式地指定它。标志 Py_TPFLAGS_BASEFUNCTION 绝不能为堆类型设置,因为这样做不安全(堆类型可以动态更改)。

C API 函数

我们列出了一些相关的 Python/C API 宏和函数。其中一些是现有的(可能已更改)函数,另一些是新的。

  • int PyBaseFunction_CheckFast(PyObject *op):如果 op 是设置了 Py_TPFLAGS_BASEFUNCTION 的类的实例,则返回 true。这是你需要用来确定访问 base_function 内部是否有意义的函数。
  • int PyBaseFunction_Check(PyObject *op):如果 opbase_function 的实例,则返回 true。
  • PyObject *PyBaseFunction_New(PyTypeObject *cls, PyCFunctionDef *ml, PyObject *self, PyObject *module, PyObject *parent):从给定数据创建一个新的 cls 实例(它必须是 base_function 的子类)。
  • int PyCFunction_Check(PyObject *op):如果 opcfunction 的实例,则返回 true。
  • int PyCFunction_NewEx(PyMethodDef* ml, PyObject *self, PyObject* module):创建一个新的 cfunction 实例。作为特例,如果 selfNULL,则改为设置 self = Py_None(为了向后兼容)。如果 self 是一个模块,则 __parent__ 被设置为 self。否则,__parent__NULL
  • 对于许多现有的 PyCFunction_...PyMethod_ 函数,我们定义了一个新的函数 PyBaseFunction_...,它作用于 base_function 实例。旧函数被保留为新函数的别名。
  • int PyFunction_Check(PyObject *op):如果 op 是设置了 METH_PYTHON 标志的 base_function 的实例,则返回 true(这等同于检查 op 是否是 function 的实例)。
  • int PyFunction_CheckFast(PyObject *op):等同于 PyFunction_Check(op) && PyBaseFunction_CheckFast(op)
  • int PyFunction_CheckExact(PyObject *op):如果 op 的类型是 function,则返回 true。
  • PyObject *PyFunction_NewPython(PyTypeObject *cls, PyObject *code, PyObject *globals, PyObject *name, PyObject *qualname):从给定数据创建一个新的 cls 实例(它必须是 function 的子类)。
  • PyObject *PyFunction_New(PyObject *code, PyObject *globals):创建一个新的 function 实例。
  • PyObject *PyFunction_NewWithQualName(PyObject *code, PyObject *globals, PyObject *qualname): 创建一个新的 function 实例。
  • PyObject *PyFunction_Copy(PyTypeObject *cls, PyObject *func): 通过复制给定的 function,创建一个新的 cls 实例(必须是 function 的子类)。

对 types 模块的更改

添加了两个类型:types.BaseFunctionType 对应于 base_functiontypes.DefinedFunctionType 对应于 defined_function

除此之外,对 types 模块没有进行任何更改。特别是,types.FunctionType 指的是 function。但是,实际类型将会发生变化:特别是,types.BuiltinFunctionType 将不再与 types.BuiltinMethodType 相同。

对 inspect 模块的更改

新函数 inspect.isbasefunction 检查 base_function 的实例。

inspect.isfunction 检查 defined_function 的实例。

inspect.isbuiltin 检查 cfunction 的实例。

inspect.isroutine 检查 isbasefunctionismethoddescriptor

注意:bpo-33261 [3] 应该首先修复。

分析

目前,sys.setprofile 支持内建函数的 c_callc_returnc_exception 事件。当调用或从内建函数返回时,会生成这些事件。相比之下,callreturn 事件是由函数本身生成的。所以,对于 callreturn 事件不需要更改任何内容。

由于我们不再区分 C 函数和 Python 函数,我们需要防止 Python 函数的 c_* 事件。如果 ml_flags 中设置了 METH_PYTHON 标志,则通过不生成这些事件来实现这一点。

非 CPython 实现

这个 PEP 的大部分内容只与 CPython 相关。对于其他 Python 实现,需要进行的两个更改是 base_function 基类,以及 function 可以被子类化的事实。类 cfunctiondefined_function 不是必需的。

我们要求 base_function 保持一致性,但我们对其没有要求:如果它只是 object 的副本是可以接受的。不需要支持新的 __parent__(和 __objclass__)属性。如果没有 defined_function 类,那么 types.DefinedFunctionType 应该成为 types.FunctionType 的别名。

基本原理

为什么不直接更改现有类?

有人可能会尝试通过保留现有的类来解决这个问题,而不引入新的 base_function 类。

这看起来可能是一个更简单的解决方案,但事实并非如此:它需要对 3 个不同的类进行内省支持:functionbuiltin_function_or_methodmethod_descriptor。对于后两个类,“内省支持”至少意味着允许子类化。但我们不想失去性能,所以我们需要快速子类检查。这将需要在 tp_flags 中添加两个新的标志。我们希望子类允许内建函数的 __get__,所以我们也应该为内建函数实现 LOAD_METHOD 操作码。更一般地说,很多功能都需要重复,最终的结果将是更复杂的代码。

也不清楚内建函数子类的内省将如何与 __text_signature__ 交互。在同一个类上拥有两种独立的 inspect.signature 支持,听起来像是自找麻烦。

而且这并不能解决在 动机 中提到的内建函数和 Python 函数之间的一些其他差异。

为什么 __text_signature__ 不是解决方案

内建函数有一个属性 __text_signature__,它以纯文本形式给出函数的签名。默认值由 ast.literal_eval 评估。因此,它只支持少量标准 Python 类,而不是任意 Python 对象。

即使 __text_signature__ 以某种方式允许任意签名,那也只是一部分内省:例如,它对 inspect.getsourcefile 没有任何帮助。

defined_function 与 function

在很多地方,需要决定是将旧的 function 类替换为 defined_function 还是新的 function 类。这是通过考虑最可能的用例来决定的。

  1. types.FunctionType 指的是 function,因为该类型可能用于使用 types.FunctionType(...) 来构造实例。
  2. inspect.isfunction() 指的是 defined_function,因为这是支持内省的类。
  3. C API 函数必须引用 function,因为我们没有指定如何实现 defined_function 的各种属性。我们预计这不会成为问题,因为通常没有理由让 C 扩展进行内省。

本 PEP 的范围:哪些类参与其中?

这个 PEP 的主要动机是修复函数类,所以我们当然希望统一现有的类 builtin_function_or_methodfunction

由于内建函数和方法具有相同的类,因此将绑定方法包括在内似乎很自然。并且由于 Python 函数没有“未绑定方法”,因此删除扩展类型的未绑定方法是有意义的。

目前,对类 staticmethodclassmethodclassmethod_descriptor 没有进行任何更改。将这些类放在 base_function 类层次结构中并统一 classmethodclassmethod_descriptor 肯定是有意义的。但是,这个 PEP 已经足够大,所以将其留作未来可能的改进。

扩展类型的槽包装器,如 __init____eq__,与普通方法有很大不同。它们通常也不会被直接调用,因为你通常会编写 foo[i] 而不是 foo.__getitem__(i)。所以这些内容不在本 PEP 的范围之内。

Python 还有一个 instancemethod 类,它似乎是 Python 2 的遗留物,在 Python 2 中它用于绑定和未绑定方法。目前还不清楚它是否还有用例。无论如何,在这个 PEP 中没有理由处理它。

待办事项instancemethod 应该被弃用吗?它似乎在 CPython 3.7 中没有被使用,但也许外部包使用了它?

不处理 METH_STATIC 和 METH_CLASS

这个 PEP 中几乎没有内容涉及标志 METH_STATICMETH_CLASS。这些标志只被 内建函数的自动创建 检查。当 staticmethodclassmethodclassmethod_descriptor 被绑定(即调用 __get__)时,会创建一个 base_function 实例,其中 m_self != NULL。对于 classmethod,这是显而易见的,因为 m_self 是方法绑定的类。对于 staticmethod,可以为 m_self 选择任意 Python 对象。为了向后兼容性,我们为扩展类型的静态方法选择 m_self = __parent__

base_function 中的 __self__

乍一看,在 base_function 中添加 __self__ 槽而不是在 bound_method 中添加,可能看起来很奇怪。我们从现有的 builtin_function_or_method 类中获得了这个想法。它允许我们对本 PEP 中讨论的各种函数类拥有 __call____get__ 的单一通用实现。

它还使得为现有的内建函数设置 __self__ 为模块变得很容易(例如,sys.exit.__self__sys)。

__doc__ 的两种实现

base_function 不支持函数文档字符串。相反,cfunctionfunction 类分别有自己的处理文档字符串的方式(而 bound_method 只是从包装的函数中获取 __doc__)。

对于 cfunction,文档字符串(连同文本签名)被存储为 PyMethodDef 的只读 ml_doc 字段中的 C 字符串。对于 function,文档字符串被存储为可写的 Python 对象,并且实际上不需要是字符串。看起来很难统一这两种处理 __doc__ 的完全不同的方法。为了向后兼容,我们保留了现有的实现。

对于 defined_function,我们要求实现 __doc__,但我们没有指定如何实现。子类可以像 cfunction 一样实现 __doc__,或者使用结构成员或其他方式。

子类化

我们禁止对 cfunctionbound_method 进行子类化,以允许对 PyCFunction_CheckPyMethod_Check 进行快速类型检查。

我们允许对其他类进行子类化,因为没有理由禁止它。对于 Python 模块,唯一相关的子类化类是 function,因为其他类无法实例化。

替换 tp_call:METH_PASS_FUNCTION 和 METH_CALL_UNBOUND

新的标志 METH_PASS_FUNCTIONMETH_CALL_UNBOUND 旨在支持以前使用自定义 tp_call 的情况。它减少了 Python/ceval.c 中用于调用对象的特殊快速路径的数量:与其分别处理 Python 函数、内置函数和方法描述符,不如只进行一次检查。

tp_call 的签名本质上是 PyBaseFunctionObject.m_ml.ml_meth 的签名,带有标志 METH_VARARGS | METH_KEYWORDS | METH_PASS_FUNCTION | METH_CALL_UNBOUND(唯一的区别是添加了一个 self 参数)。因此,应该很容易将现有的 tp_call 槽改为使用 base_function 实现。

在 C 函数只需要访问函数的额外元数据(例如 __parent__)的情况下,使用 METH_PASS_FUNCTION 但不使用 METH_CALL_UNBOUND 也有意义。例如,这需要支持 PEP 573。将现有方法转换为使用 METH_PASS_FUNCTION 很简单:只需要在 C 函数中添加一个额外的参数。

向后兼容性

在设计这个 PEP 时,非常注意不要过分破坏向后兼容性。大多数可能不兼容的更改是对 CPython 实现细节的更改,这些细节在其他 Python 解释器中是不同的。特别是,在 PyPy 上正确运行的 Python 代码很可能继续与这个 PEP 一起工作。

标准类和函数,例如 staticmethodfunctools.partialoperator.methodcaller,根本不需要更改。

对 types 和 inspect 的更改

typesinspect 提出的更改旨在最大限度地减少行为上的更改。然而,不可避免地会有一些东西发生变化,这会导致使用 typesinspect 的代码崩溃。例如,在 Python 标准库中,由于这一点,doctest 模块需要进行更改。

此外,将各种类型的函数作为输入的工具将需要处理新的函数层次结构以及自定义函数类的可能性。

Python 函数

对于 Python 函数,基本上没有什么变化。以前存在的属性仍然存在,Python 函数可以像以前一样初始化、调用和转换为方法。

名称 function 保持为向后兼容。虽然将名称更改为更具体的名称(例如 python_function)可能更有意义,但这将需要在文档和测试套件中进行大量令人厌烦的更改。

模块的内置函数

对于内置函数,也没有任何变化。我们保留了这些函数不绑定为方法的旧行为。这是 __self__ 设置为模块的结果。

内置绑定和未绑定方法

内置绑定方法和未绑定方法的类型将发生变化。但是,这不会影响调用这些方法,因为 base_function.__call__ 中的协议(特别是 __objclass__ 和自身切片的处理)专门设计为向后兼容。所有以前存在的属性(如 __objclass____self__)仍然存在。

新属性

一些对象获得新的特殊双下划线属性。例如,新的属性 __parent__ 出现在所有内置函数上,所有方法都获得 __func__ 属性。__self__ 现在是 Python 函数的特殊只读属性,这一事实导致了 [4] 中的麻烦。一般来说,我们预计不会造成太多破坏。

method_descriptor 和 PyDescr_NewMethod

method_descriptor 和构造函数 PyDescr_NewMethod 应该被弃用。它们不再被 CPython 本身使用,但仍然得到支持。

两阶段实施

待办事项:本节是可选的。如果这个 PEP 被接受,应该决定是否采用这种两阶段实现。

如上所述,对 types 和 inspect 的更改 会破坏一些现有代码。为了进一步减少破坏,这个 PEP 可以分两个阶段实现。

第一阶段:保留现有类,但添加基类

最初,实现 base_function 类并将其用作通用基类,但除此之外保留现有类(但不保留它们的实现)。

在这个提议中,类层次结构将变为

                      object
                         |
                         |
                  base_function
                 /       |     \
                /        |      \
               /         |       \
      cfunction          |     defined_function
       |     |           |         \
       |     |      bound_method    \
       |     |                       \
       |  method_descriptor       function
       |
builtin_function_or_method

叶子类 builtin_function_or_methodmethod_descriptorbound_methodfunction 对应于现有类(将 method 重命名为 bound_method)。

在模块中自动创建的函数成为 builtin_function_or_method 的实例。扩展类型的未绑定方法成为 method_descriptor 的实例。

method_descriptorcfunction 的副本,除了 __get__ 返回 builtin_function_or_method 而不是 bound_method

builtin_function_or_methodbound_method 具有相同的 C 结构,但它继承自 cfunction。属性 __func__ 不是必需的:它只在绑定 method_descriptor 时定义。

我们保留 inspect 函数的实现原样。由于这一点以及保留了现有类,所以对于进行类型检查的代码来说,向后兼容性得到了保证。

由于显示实际的 DeprecationWarning 会影响很多正确运行的代码,因此任何弃用都只会出现在文档中。另一个原因是,很难显示调用 isinstance(x, t) 的警告(但可以使用 __instancecheck__ 技巧完成),而对于 type(x) is t 来说是不可能的。

第二阶段

第二阶段是这个 PEP 中其余部分实际描述的内容。就实现而言,与第一阶段相比,它将是一个相对较小的更改。

参考实现

这个 PEP 的大部分内容已经在 CPython 中实现了,网址为 https://github.com/jdemeyer/cpython/tree/pep575

有四个步骤,对应于该分支上的提交。在每个步骤之后,CPython 处于大部分工作状态。

  1. 添加 base_function 类并使其成为 cfunction 的子类。这是迄今为止最大的步骤,因为完整的 __call__ 协议在此步骤中实现。
  2. method 重命名为 bound_method 并使其成为 base_function 的子类。将扩展类型的未绑定方法更改为 cfunction 的实例,以便扩展类型的绑定方法也是 bound_method 的实例。
  3. 实现 defined_functionfunction
  4. 对 Python 的其他部分进行更改,例如标准库和测试套件。

附录:当前情况

注意:本节在 PEP 草案阶段更有用,因此如果 PEP 被接受,请随时将其删除。

为了便于参考,我们详细描述了 CPython 3.7 中相关的现有类。

每个涉及的类都是一个“孤儿”类(没有非平凡的子类或超类)。

builtin_function_or_method:内置函数和绑定方法

这些是类型 PyCFunction_Type,结构为 PyCFunctionObject

typedef struct {
    PyObject_HEAD
    PyMethodDef *m_ml; /* Description of the C function to call */
    PyObject    *m_self; /* Passed as 'self' arg to the C func, can be NULL */
    PyObject    *m_module; /* The __module__ attribute, can be anything */
    PyObject    *m_weakreflist; /* List of weak references */
} PyCFunctionObject;

struct PyMethodDef {
    const char  *ml_name;   /* The name of the built-in function/method */
    PyCFunction ml_meth;    /* The C function that implements it */
    int         ml_flags;   /* Combination of METH_xxx flags, which mostly
                               describe the args expected by the C func */
    const char  *ml_doc;    /* The __doc__ attribute, or NULL */
};

其中 PyCFunction 是一个 C 函数指针(有多种形式,最基本的形式接受两个参数:self*args)。

此类用于函数和绑定方法:对于方法,m_self 槽指向对象。

>>> dict(foo=42).get
<built-in method get of dict object at 0x...>
>>> dict(foo=42).get.__self__
{'foo': 42}

在某些情况下,函数被认为是定义它的模块的“方法”。

>>> import os
>>> os.kill
<built-in function kill>
>>> os.kill.__self__
<module 'posix' (built-in)>

method_descriptor:内置未绑定方法

这些是类型 PyMethodDescr_Type,结构为 PyMethodDescrObject

typedef struct {
    PyDescrObject d_common;
    PyMethodDef *d_method;
} PyMethodDescrObject;

typedef struct {
    PyObject_HEAD
    PyTypeObject *d_type;
    PyObject *d_name;
    PyObject *d_qualname;
} PyDescrObject;

function:Python 函数

这些是类型 PyFunction_Type,结构为 PyFunctionObject

typedef struct {
    PyObject_HEAD
    PyObject *func_code;        /* A code object, the __code__ attribute */
    PyObject *func_globals;     /* A dictionary (other mappings won't do) */
    PyObject *func_defaults;    /* NULL or a tuple */
    PyObject *func_kwdefaults;  /* NULL or a dict */
    PyObject *func_closure;     /* NULL or a tuple of cell objects */
    PyObject *func_doc;         /* The __doc__ attribute, can be anything */
    PyObject *func_name;        /* The __name__ attribute, a string object */
    PyObject *func_dict;        /* The __dict__ attribute, a dict or NULL */
    PyObject *func_weakreflist; /* List of weak references */
    PyObject *func_module;      /* The __module__ attribute, can be anything */
    PyObject *func_annotations; /* Annotations, a dict or NULL */
    PyObject *func_qualname;    /* The qualified name */

    /* Invariant:
     *     func_closure contains the bindings for func_code->co_freevars, so
     *     PyTuple_Size(func_closure) == PyCode_GetNumFree(func_code)
     *     (func_closure may be NULL if PyCode_GetNumFree(func_code) == 0).
     */
} PyFunctionObject;

在 Python 3 中,没有“未绑定方法”类:未绑定方法只是一个普通函数。

method:Python 绑定方法

这些是类型 PyMethod_Type,结构为 PyMethodObject

typedef struct {
    PyObject_HEAD
    PyObject *im_func;   /* The callable object implementing the method */
    PyObject *im_self;   /* The instance it is bound to */
    PyObject *im_weakreflist; /* List of weak references */
} PyMethodObject;

参考文献


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

最后修改:2023-09-09 17:39:29 GMT