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

Python 增强提案

PEP 252 – 让类型更像类

作者:
Guido van Rossum <guido at python.org>
状态:
最终版
类型:
标准跟踪
创建日期:
2001年4月19日
Python 版本:
2.2
发布历史:


目录

摘要

本PEP提议对类型的自省API进行修改,使它们更像类,它们的实例更像类实例。例如,对于大多数内置类型,type(x) 将等同于 x.__class__。当 C 是 x.__class__ 时,x.meth(a) 通常等同于 C.meth(x, a),并且 C.__dict__ 包含 x 的方法和其他属性。

本PEP还引入了一种指定属性的新方法,使用属性描述符,简称描述符。描述符统一并概括了几种用于描述属性的不同常见机制:描述符可以描述方法、对象结构中的类型化字段,或者由 getter 和 setter 函数表示的通用属性。

基于通用描述符API,本PEP还引入了一种声明类方法和静态方法的方式。

[编者注:本PEP中描述的想法已融入Python。本PEP不再准确描述其实现。]

引言

Python最古老的语言缺陷之一是类和类型之间的区别。例如,你不能直接子类化字典类型,并且用于查找对象具有哪些方法和实例变量的自省接口对于类型和类是不同的。

弥合类/类型分歧是一项艰巨的任务,因为它影响到Python实现的许多方面。本PEP关注于使类型的自省API与类的自省API看起来相同。其他PEP将提议使类更像类型,并从内置类型进行子类化;这些主题不属于本PEP的讨论范围。

自省API

自省关注于查找对象具有哪些属性。Python非常通用的 getattr/setattr API使得无法保证总有一种方法可以获取特定对象支持的所有属性的列表,但实际上出现了两种约定,它们一起适用于几乎所有对象。我将它们称为基于类的自省API和基于类型的自省API;简称类API和类型API。

基于类的自省API主要用于类实例;它也被Jim Fulton的ExtensionClasses使用。它假设对象 x 的所有数据属性都存储在字典 x.__dict__ 中,并且所有方法和类变量都可以通过检查 x 的类(写为 x.__class__)来找到。类有一个 __dict__ 属性,它产生一个包含类本身定义的方法和类变量的字典,以及一个 __bases__ 属性,它是一个必须递归检查的基类元组。这里的一些假设是

  • 实例字典中定义的属性会覆盖对象类中定义的属性;
  • 派生类中定义的属性会覆盖基类中定义的属性;
  • 较早的基类中定义的属性(即在 __bases__ 中较早出现的)会覆盖较晚的基类中的属性。

(后两条规则通常一起概括为属性搜索的从左到右、深度优先规则。这是经典的Python属性查找规则。请注意,PEP 253 将提议更改属性查找顺序,如果被接受,本PEP将随之调整。)

大多数内置对象都以某种形式支持基于类型的自省API。它使用两个特殊属性,__members____methods____methods__ 属性(如果存在)是对象支持的方法名称列表。__members__ 属性(如果存在)是对象支持的数据属性名称列表。

类型API有时会与一个与实例工作方式相同的 __dict__ 结合使用(例如,对于Python 2.1中的函数对象,f.__dict__ 包含 f 的动态属性,而 f.__members__ 列出了 f 的静态定义属性的名称)。

必须谨慎:有些对象不将它们的“固有”属性(如 __dict____doc__)列在 __members__ 中,而另一些则会;有时属性名称同时出现在 __members____methods__ 中以及作为 __dict__ 中的键,在这种情况下,__dict__ 中找到的值是否使用是不可预测的。

类型API从未被仔细指定。它是Python民间传说的一部分,大多数第三方扩展都支持它,因为它们遵循支持它的示例。此外,任何在其 tp_getattr 处理程序中使用 Py_FindMethod() 和/或 PyMember_Get() 的类型都支持它,因为这两个函数分别对属性名称 __methods____members__ 进行特殊处理。

Jim Fulton的ExtensionClasses忽略了类型API,而是模拟了功能更强大的类API。在本PEP中,我提议逐步淘汰类型API,转而支持所有类型的类API。

支持类API的一个论点是,它不需要你创建一个实例来找出类型支持哪些属性;这反过来又对文档处理器很有用。例如,socket 模块导出了 SocketType 对象,但它目前没有告诉我们 socket 对象上定义了哪些方法。使用类API,SocketType 将准确显示 socket 对象的方法是什么,我们甚至可以提取它们的文档字符串,而无需创建 socket。(由于这是一个 C 扩展模块,在这种情况下,源代码扫描的文档字符串提取方法是不可行的。)

基于类的自省API规范

对象可以有两种属性:静态和动态。静态属性的名称和有时其他属性可以通过检查对象的类型或类来得知,这些类型或类可通过 obj.__class__type(obj) 访问。(我将类型和类互换使用;一个笨拙但描述性的术语是“元对象”。)

(XXX 静态和动态在这里不是很好的术语,因为“静态”属性实际上可能表现得很动态,而且它们与 C++ 或 Java 中的静态类成员无关。Barry 建议改用不可变和可变,但这些词在稍微不同的上下文中已经有精确且不同的含义,所以我觉得那仍然会令人困惑。)

动态属性的示例包括类实例的实例变量、模块属性等。静态属性的示例包括列表和字典等内置对象的方法,以及帧和代码对象的属性(f.f_codec.co_filename 等)。当一个具有动态属性的对象通过其 __dict__ 属性公开这些属性时,__dict__ 是一个静态属性。

动态属性的名称和值通常存储在字典中,并且该字典通常可以通过 obj.__dict__ 访问。本规范的其余部分更关注发现静态属性的名称和属性,而不是动态属性;后者很容易通过检查 obj.__dict__ 来发现。

在下面的讨论中,我区分两种对象:普通对象(如列表、整数、函数)和元对象。类型和类是元对象。元对象也是普通对象,但我们主要对它们感兴趣,因为它们被普通对象的 __class__ 属性(或其他元对象的 __bases__ 属性)引用。

类自省API包含以下元素

  • 普通对象上的 __class____dict__ 属性;
  • 元对象上的 __bases____dict__ 属性;
  • 优先级规则;
  • 属性描述符。

这些共同不仅告诉我们元对象定义了**所有**属性,而且还帮助我们计算给定对象的特定属性的值。

  1. 普通对象上的 __dict__ 属性

    普通对象可能具有 __dict__ 属性。如果存在,这应该是一个映射(不一定是字典),至少支持 __getitem__()keys()has_key()。这提供了对象的动态属性。映射中的键给出属性名称,相应的值给出它们的值。

    通常,具有给定名称的属性的值与该名称在 __dict__ 中作为键对应的值是相同的对象。换句话说,obj.__dict__['spam'] 就是 obj.spam。(但请参见下面的优先级规则;具有相同名称的静态属性**可能**覆盖字典项。)

  2. 普通对象上的 __class__ 属性

    普通对象通常有一个 __class__ 属性。如果存在,它引用一个元对象。元对象可以为其 __class__ 是它的普通对象定义静态属性。这通常通过以下机制完成

  3. 元对象上的 __dict__ 属性

    元对象可以有一个 __dict__ 属性,其形式与普通对象的 __dict__ 属性相同(一个映射但不必是字典)。如果存在,元对象的 __dict__ 的键是相应普通对象的静态属性名称。值是属性描述符;我们稍后会解释这些。未绑定方法是属性描述符的一种特殊情况。

    因为元对象也是一个普通对象,所以元对象的 __dict__ 中的项对应于元对象的属性;但是,可能会应用一些转换,并且基类(参见下文)可能会定义额外的动态属性。换句话说,mobj.spam 并不总是 mobj.__dict__['spam']。(此规则包含一个漏洞,因为对于类,如果 C.__dict__['spam'] 是一个函数,C.spam 是一个未绑定方法对象。)

  4. 元对象上的 __bases__ 属性

    元对象可能具有 __bases__ 属性。如果存在,这应该是一个包含其他元对象(基类)的序列(不一定是元组)。没有 __bases__ 等同于一个空的基类序列。由 __bases__ 属性定义的元对象之间的关系中绝不能有循环;换句话说,__bases__ 属性定义了一个有向无环图,其中弧线从派生元对象指向其基元对象。(它不一定是树,因为多个类可以有相同的基类。)继承图中元对象的 __dict__ 属性为普通对象提供属性描述符,其 __class__ 属性指向继承树的根(这与继承层次结构的根不同——更确切地说是相反的,因为继承树通常是向下绘制的)。描述符首先在根元对象的字典中搜索,然后根据优先级规则(参见下一段)在其基类中搜索。

  5. 优先级规则

    当给定普通对象的继承图中两个元对象都定义了具有相同名称的属性描述符时,搜索顺序由元对象决定。这允许不同的元对象定义不同的搜索顺序。特别是,经典类使用旧的从左到右深度优先规则,而新式类使用更高级的规则(请参见 PEP 253 中关于方法解析顺序的部分)。

    当动态属性(在普通对象的 __dict__ 中定义的属性)与静态属性(由继承图中以普通对象的 __class__ 为根的元对象定义的属性)具有相同名称时,如果静态属性是定义了 __set__ 方法的描述符(参见下文),则静态属性具有优先级;否则(如果没有 __set__ 方法),动态属性具有优先级。换句话说,对于数据属性(那些具有 __set__ 方法的属性),静态定义会覆盖动态定义,但对于其他属性,动态会覆盖静态。

    基本原理:我们不能有一个简单的规则,如“静态覆盖动态”或“动态覆盖静态”,因为某些静态属性确实覆盖动态属性;例如,实例的 __dict__ 中键为“__class__”的项会被忽略,而优先使用静态定义的 __class__ 指针,但另一方面,inst.__dict__ 中的大多数键会覆盖 inst.__class__ 中定义的属性。描述符上存在 __set__ 方法表示这是一个数据描述符。(即使是只读数据描述符也有一个 __set__ 方法:它总是引发异常。)描述符上不存在 __set__ 方法表示描述符对拦截赋值不感兴趣,然后应用经典规则:与方法同名的实例变量会隐藏该方法,直到它被删除。

  6. 属性描述符

    这正是有趣——也混乱——的地方。属性描述符(简称描述符)存储在元对象的 __dict__ 中(或其某个祖先的 __dict__ 中),具有两个用途:描述符可用于获取或设置(普通、非元)对象上的相应属性值,并且它还具有一个附加接口,用于文档和自省目的描述属性。

    Python中很少有设计描述符接口的先例,无论是用于获取/设置值还是以其他方式描述属性,除了一些微不足道的属性(假设 __name____doc__ 应该是属性的名称和文档字符串是合理的)。我将在下面提出这样一个API。

    如果在元对象的 __dict__ 中找到的对象不是属性描述符,则为了向后兼容性,必须规定某些最小语义。这基本上意味着,如果它是一个 Python 函数或未绑定方法,则该属性是一个方法;否则,它是动态数据属性的默认值。向后兼容性还规定(在没有 __setattr__ 方法的情况下),允许赋值给与方法对应的属性,并且这会创建一个数据属性,该属性会遮蔽此特定实例的方法。但是,这些语义仅在与常规类向后兼容时才需要。

自省API是一个只读API。我们不定义对任何特殊属性(__dict____class____bases__)的赋值效果,也不定义对 __dict__ 项的赋值效果。一般来说,此类赋值应被视为禁止。未来的PEP可能会定义一些此类赋值的语义。(特别是目前实例支持对 __class____dict__ 的赋值,类支持对 __bases____dict__ 的赋值。)

属性描述符API规范

属性描述符可能具有以下属性。在示例中,x 是一个对象,C 是 x.__class__x.meth() 是一个方法,x.ivar 是一个数据属性或实例变量。所有属性都是可选的——特定属性可能存在也可能不存在于给定描述符上。缺少的属性表示相应的信息不可用或相应的功能未实现。

  • __name__:属性名称。由于别名和重命名,该属性可能(额外地或专门地)以不同的名称为人所知,但这是它诞生的名称。示例:C.meth.__name__ == 'meth'
  • __doc__:属性的文档字符串。这可以是 None。
  • __objclass__:声明此属性的类。描述符仅适用于此类的实例(包括其子类的实例)。示例:C.meth.__objclass__ is C
  • __get__():一个可带一个或两个参数的函数,用于从对象中检索属性值。这也称为“绑定”操作,因为在方法描述符的情况下,它可能返回一个“绑定方法”对象。第一个参数 X 是必须从中检索属性或必须绑定属性的对象。当 X 为 None 时,可选的第二个参数 T 应该是元对象,并且绑定操作可以返回一个受限于 T 实例的**未绑定**方法。当 X 和 T 都指定时,X 应该是 T 的实例。绑定操作返回的确切内容取决于描述符的语义;例如,静态方法和类方法(参见下文)会忽略实例并绑定到类型。
  • __set__():一个带两个参数的函数,用于在对象上设置属性值。如果属性是只读的,此方法可能会引发 TypeError 或 AttributeError 异常(两者都允许,因为历史上对于未定义或不可设置的属性都曾出现过)。示例:C.ivar.set(x, y) ~~ x.ivar = y

静态方法和类方法

描述符 API 使得添加静态方法和类方法成为可能。静态方法很容易描述:它们的行为与 C++ 或 Java 中的静态方法非常相似。这是一个例子

class C:

    def foo(x, y):
        print "staticmethod", x, y
    foo = staticmethod(foo)

C.foo(1, 2)
c = C()
c.foo(1, 2)

调用 C.foo(1, 2) 和调用 c.foo(1, 2) 都会用两个参数调用 foo(),并打印“staticmethod 1 2”。在 foo() 的定义中没有声明“self”,在调用中也不需要实例。

类语句中的“foo = staticmethod(foo)”这一行是关键元素:它使 foo() 成为一个静态方法。内置的 staticmethod() 将其函数参数包装在一个特殊类型的描述符中,其 __get__() 方法返回未更改的原始函数。没有它,标准函数对象的 __get__() 方法将为“c.foo”创建一个绑定方法对象,并为“C.foo”创建一个未绑定方法对象。

(XXX Barry 建议用“sharedmethod”代替“staticmethod”,因为“static”这个词已经以多种方式被过载了。但我不确定“shared”是否传达了正确的含义。)

类方法使用类似的模式来声明接收一个隐式第一个参数的方法,该参数是调用它们的*类*。这在C++或Java中没有等价物,也与Smalltalk中的类方法不完全相同,但可能服务于类似的目的。根据Armin Rigo的说法,它们类似于Borland Pascal方言Delphi中的“虚拟类方法”。(Python也有真正的元类,也许在元类中定义的方法更有资格被称为“类方法”;但我预计大多数程序员不会使用元类。)这是一个例子

class C:

    def foo(cls, y):
        print "classmethod", cls, y
    foo = classmethod(foo)

C.foo(1)
c = C()
c.foo(1)

调用 C.foo(1) 和调用 c.foo(1) 最终都使用**两个**参数调用 foo(),并打印“classmethod __main__.C 1”。foo() 的第一个参数是隐式的,它是类,即使该方法是通过实例调用的。现在让我们继续这个例子

class D(C):
    pass

D.foo(1)
d = D()
d.foo(1)

这两次都打印“classmethod __main__.D 1”;换句话说,作为 foo() 的第一个参数传递的类是参与调用的类,而不是参与 foo() 定义的类。

但请注意这一点

class E(C):
    def foo(cls, y): # override C.foo
        print "E.foo() called"
        C.foo(y)
    foo = classmethod(foo)

E.foo(1)
e = E()
e.foo(1)

在这个例子中,从 E.foo() 调用 C.foo() 时,它的第一个参数将是类 C,而不是类 E。这是预料之中的,因为调用指定了类 C。但这强调了这些类方法与在元类中定义的方法之间的区别,在元类中,对元方法的上调会以显式第一个参数传递目标类。(如果你不理解这一点,不用担心,你不是唯一一个。)请注意,调用 cls.foo(y) 是一个错误——它会导致无限递归。另请注意,你不能为类方法指定显式的“cls”参数。如果你需要这样做(例如 PEP 253 中的 __new__ 方法需要),请改用以类作为其显式第一个参数的静态方法。

C API

XXX 以下是我为不同受众撰写的非常粗糙的文本;我需要重新编辑它。XXX 它也没有深入到C API的足够细节。

内置类型可以通过两种方式声明特殊数据属性:使用结构成员列表(在 structmember.h 中定义)或结构 getsetlist(在 descrobject.h 中定义)。结构成员列表是一种旧机制的新用途:每个属性都有一个描述符记录,包括其名称、一个给出其类型的枚举(支持各种 C 类型以及 PyObject *)、一个从实例开头开始的偏移量和一个只读标志。

struct getsetlist 机制是新的,适用于不符合上述模式的情况,因为它们需要额外的检查,或者只是计算属性。这里的每个属性都有一个名称、一个 getter C 函数指针、一个 setter C 函数指针和一个上下文指针。函数指针是可选的,因此例如将 setter 函数指针设置为 NULL 会使属性变为只读。上下文指针旨在将辅助信息传递给通用 getter/setter 函数,但我尚未发现对此有需求。

请注意,还有一种类似的机制可以声明内置方法:它们是 PyMethodDef 结构,其中包含一个名称和一个 C 函数指针(以及一些用于调用约定的标志)。

传统上,内置类型必须定义自己的 tp_getattrotp_setattro 插槽函数,以使这些属性定义生效(PyMethodDef 和 struct memberlist 相当古老)。有一些便利函数,它们接受一个 PyMethodDef 或 memberlist 结构数组、一个对象和一个属性名称,如果找到列表中则返回或设置属性,否则引发异常。但是这些便利函数必须由特定类型的 tp_getattrotp_setattro 方法显式调用,并且它们使用 strcmp() 对数组进行线性搜索以找到描述请求属性的数组元素。

我现已拥有一套全新的通用机制,可显著改善这种情况。

  • PyMethodDef、memberlist、getsetlist 结构数组的指针是新类型对象的一部分(tp_methodstp_memberstp_getset)。
  • 在类型初始化时(在 PyType_InitDict() 中),对于这三个数组中的每个条目,都会创建一个描述符对象并放置在属于该类型(tp_dict)的字典中。
  • 描述符是非常精简的对象,它们主要指向相应的结构。一个实现细节是所有描述符共享相同的对象类型,并且一个判别字段指示它是哪种描述符(方法、成员或getset)。
  • 正如 PEP 252 中所解释的,描述符有一个 get() 方法,它接受一个对象参数并返回该对象的属性;可写属性的描述符还有一个 set() 方法,它接受一个对象和一个值并设置该对象的属性。请注意,get() 对象也作为方法的 bind() 操作,将未绑定方法实现绑定到对象。
  • 现在,几乎所有内置对象都将 PyObject_GenericGetAttr 和(如果它们有任何可写属性)PyObject_GenericSetAttr 放在它们的 tp_getattrotp_setattro 插槽中,而不是提供自己的 tp_getattro 和 tp_setattro 实现。(或者,它们可以将其保留为 NULL,并从默认基对象继承它们,如果它们在创建第一个实例之前安排显式调用 PyType_InitDict() 来初始化类型。)
  • 在最简单的情况下,PyObject_GenericGetAttr() 仅执行一次字典查找:它在类型的字典中查找属性名称(obj->ob_type->tp_dict)。成功后,有两种可能性:描述符有一个 get 方法,或者没有。为了提高速度,get 和 set 方法是类型槽:tp_descr_gettp_descr_set。如果 tp_descr_get 槽不为 NULL,则调用它,将对象作为其唯一参数,此调用的返回值是 getattr 操作的结果。如果 tp_descr_get 槽为 NULL,则作为后备,描述符本身被返回(比较那些不是方法而是简单值的类属性)。
  • PyObject_GenericSetAttr() 工作方式非常相似,但使用 tp_descr_set 槽并使用对象和新的属性值调用它;如果 tp_descr_set 槽为 NULL,则会引发 AttributeError
  • 但现在来看一个更复杂的情况。上述方法适用于大多数内置对象,如列表、字符串、数字。然而,有些对象类型在每个实例中都有一个字典,可以存储任意属性。事实上,当你使用类语句对现有内置类型进行子类型化时,你会自动获得这样一个字典(除非你使用另一个高级功能 __slots__ 明确关闭它)。我们称之为实例字典,以区别于类型字典。
  • 在更复杂的情况下,实例字典中存储的名称与类型字典中存储的名称之间存在冲突。如果两个字典都有一个具有相同键的条目,我们应该返回哪一个?回顾经典的 Python,我发现相互冲突的规则:对于类实例,实例字典会覆盖类字典,**除了**特殊属性(如 __dict____class__),它们优先于实例字典。
  • 我使用以下规则集解决了这个问题,该规则集在 PyObject_GenericGetAttr() 中实现
    1. 查看类型字典。如果你找到了一个**数据**描述符,则使用其 get() 方法生成结果。这解决了像 __dict____class__ 这样的特殊属性。
    2. 查看实例字典。如果你找到了任何东西,那就是它了。(这解决了通常实例字典会覆盖类字典的要求。)
    3. 再次查看类型字典(当然,实际上这使用的是步骤 1 中保存的结果)。如果你找到了一个描述符,则使用其 get() 方法;如果你找到了其他东西,那就是它了;如果不存在,则引发 AttributeError

    这需要将描述符分类为数据描述符和非数据描述符。当前的实现非常合理地将成员描述符和 getset 描述符分类为数据(即使它们是只读的!),并将方法描述符分类为非数据。非描述符(如函数指针或普通值)也被分类为非数据(!)。

  • 这个方案有一个缺点:在我看来最常见的情况是,引用存储在实例字典中的实例变量时,它进行**两次**字典查找,而经典方案对以两个下划线开头的属性进行快速测试,然后进行一次字典查找。(尽管实现令人遗憾地构建为 instance_getattr() 调用 instance_getattr1() 调用 instance_getattr2(),最后调用 PyDict_GetItem(),并且下划线测试调用 PyString_AsString() 而不是内联它。我想知道,如果我们不打算全部移除,那么对它进行极致优化以加速 Python 2.2 是不是一个好主意。:-)
  • 基准测试证实这实际上与经典的实例变量查找一样快,所以我不再担心了。
  • 动态类型修改:步骤1和3查找类型及其所有基类的字典(当然,按MRO顺序)。

讨论

XXX

示例

让我们看看列表。在经典的Python中,列表的方法名可以通过列表对象的__methods__属性获得

>>> [].__methods__
['append', 'count', 'extend', 'index', 'insert', 'pop',
'remove', 'reverse', 'sort']
>>>

根据新提案,__methods__属性不再存在

>>> [].__methods__
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
AttributeError: 'list' object has no attribute '__methods__'
>>>

相反,您可以从列表类型中获取相同的信息

>>> T = [].__class__
>>> T
<type 'list'>
>>> dir(T)                # like T.__dict__.keys(), but sorted
['__add__', '__class__', '__contains__', '__eq__', '__ge__',
'__getattr__', '__getitem__', '__getslice__', '__gt__',
'__iadd__', '__imul__', '__init__', '__le__', '__len__',
'__lt__', '__mul__', '__ne__', '__new__', '__radd__',
'__repr__', '__rmul__', '__setitem__', '__setslice__', 'append',
'count', 'extend', 'index', 'insert', 'pop', 'remove',
'reverse', 'sort']
>>>

新的自省API比旧的提供更多信息:除了常规方法,它还显示了通常通过特殊符号调用的方法,例如 __iadd__ (+=)、__len__ (len)、__ne__ (!=)。您可以直接调用此列表中的任何方法

>>> a = ['tic', 'tac']
>>> T.__len__(a)          # same as len(a)
2
>>> T.append(a, 'toe')    # same as a.append('toe')
>>> a
['tic', 'tac', 'toe']
>>>

这与用户定义的类一样。

请注意列表中一个熟悉但令人惊讶的名称:__init__。这是 PEP 253 的领域。

向后兼容性

XXX

警告和错误

XXX

实施

本 PEP 的部分实现可从 CVS 获取,分支名为“descr-branch”。要试验此实现,请根据 http://sourceforge.net/cvs/?group_id=5470 的说明从 CVS 检出 Python,但请在 cvs checkout 命令中添加参数“-r descr-branch”。(你也可以从现有检出开始,然后执行“cvs update -r descr-branch”。)有关此处描述的功能的一些示例,请参见文件 Lib/test/test_descr.py。

注意:此分支中的代码远远超出了本 PEP 的范围;它也是 PEP 253(子类化内置类型)的实验区。

参考资料

XXX


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

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