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的静态定义属性的名称)。

必须谨慎行事:某些对象不会在其__members__中列出其“内在”属性(如__dict____doc__),而其他对象则会列出;有时属性名称既出现在__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对象的方法是什么,我们甚至可以提取它们的文档字符串,而无需创建套接字。(由于这是一个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 进行足够的详细说明。XXX

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

结构体 getset 列表机制是新的,用于那些不适合这种模式的情况,因为它们要么需要额外的检查,要么是纯粹的计算属性。这里的每个属性都有一个名称、一个 getter C 函数指针、一个 setter C 函数指针和一个上下文指针。函数指针是可选的,例如,将 setter 函数指针设置为 NULL 会创建一个只读属性。上下文指针旨在将辅助信息传递给通用 getter/setter 函数,但我还没有发现对此的需要。

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

传统上,内置类型必须定义自己的 tp_getattrotp_setattro 槽函数才能使这些属性定义生效(PyMethodDef 和结构体成员列表都比较旧)。有一些便利函数可以接收 PyMethodDef 或成员列表结构的数组、一个对象和一个属性名称,并在列表中找到该属性时返回或设置它,或者在未找到时引发异常。但这些便利函数必须由特定类型的 tp_getattrotp_setattro 方法显式调用,并且它们使用 strcmp() 对数组进行线性搜索以找到描述请求属性的数组元素。

我现在有一个全新的通用机制,可以大大改善这种情况。

  • PyMethodDef、成员列表、getset 列表结构的数组指针是新类型对象的一部分(tp_methodstp_memberstp_getset)。
  • 在类型初始化时(在 PyType_InitDict() 中),对于这三个数组中的每个条目,都会创建一个描述符对象并将其放置在属于该类型的字典中(tp_dict)。
  • 描述符是非常精简的对象,主要指向相应的结构。一个实现细节是,所有描述符共享相同的对象类型,并且一个判别字段指示它是哪种类型的描述符(方法、成员或 getset)。
  • PEP 252 中所述,描述符具有一个 get() 方法,该方法接收一个对象参数并返回该对象的属性;可写属性的描述符还具有一个 set() 方法,该方法接收一个对象和一个值并设置该对象的属性。请注意,get() 对象也充当方法的 bind() 操作,将未绑定的方法实现绑定到对象上。
  • 几乎所有内置对象现在都将 PyObject_GenericGetAttr 和(如果它们有任何可写属性)PyObject_GenericSetAttr 放置在其 tp_getattrotp_setattro 槽中,而不是提供自己的 tp_getattrotp_setattro 实现。(或者,如果它们在创建第一个实例之前为该类型安排了对 PyType_InitDict() 的显式调用,它们可以将这些槽设置为 NULL 并从默认基对象继承它们。)
  • 在最简单的情况下,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 提供了比旧 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,但将参数“-r descr-branch”添加到 cvs checkout 命令。(您也可以从现有检出开始,并执行“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

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