注意: 虽然 JavaScript 对于本网站不是必需的,但您与内容的互动将受到限制。请开启 JavaScript 以获得完整的体验。

在 Python 2.2 中统一类型和类

Python 版本:2.2.3

吉多·范罗苏姆

本文档是一个不完整的草稿。我正在征求反馈意见。如果您发现任何问题,请发送邮件至 guido@python.org 给我。

目录

更改日志

自本教程的原始 Python 2.2 版本以来的更改

  • 不要通过暗示类方法可能会消失来吓唬人。(2002年4月4日)

引言

Python 2.2引入了“类型/类统一”的第一阶段。这是一系列对Python的更改,旨在消除内置类型和用户定义类之间的大部分差异。其中最明显的一点可能是取消了在类语句中将内置类型(如列表和字典的类型)用作基类的限制。

这是Python有史以来最大的变化之一,但它的向后不兼容性却非常少。这些更改在一系列 PEP(Python增强提案)中详细描述。PEP并非旨在作为教程,并且描述类型/类统一的PEP有时难以阅读。它们也尚未完成。这就是本文的作用:它为普通Python程序员介绍了类型/类统一的关键元素。

一些术语:“经典Python”指的是Python 2.1(及其补丁版本如2.1.1)或更早的版本,而“经典类”指的是在类语句中定义的类,其基类中不包含内置对象:要么因为它没有基类,要么因为它所有的基类本身都是经典类——递归地应用这个定义。

经典类在Python 2.2中仍然是一个特殊的类别。最终它们将与类型完全统一,但由于额外的向后不兼容性,这将在2.2发布之后完成(可能不会早于Python 3.0)。我将尝试在指代内置类型时说“类型”,在指代经典类或两者皆可时说“类”;如果上下文不明确哪个解释是意指的,我将尝试明确地使用“经典类”或“类或类型”。

内置类型的子类化

让我们从最有趣的部分开始:您可以对字典和列表等内置类型进行子类型化。您所需要的只是一个内置类型基类的名称,然后就可以开始了。

有一个新的内置名称“dict”,用于字典类型。(在2.2b1及更早版本中,这被称为“dictionary”;虽然我通常不喜欢缩写,“dictionary”实在太长了,我们已经说“dict”很多年了。)

这实际上只是语法糖,因为已经有两种其他方式来命名这种类型:type({}) 和(导入 types 模块后)types.DictType(以及第三种,types.DictionaryType)。但既然类型扮演着更核心的角色,为那些你可能遇到的类型提供内置名称似乎是合适的。

这是一个简单的 dict 子类的示例,它提供了一个“默认值”,在请求缺失的键时返回

class defaultdict(dict):

    def __init__(self, default=None):
        dict.__init__(self)
        self.default = default

    def __getitem__(self, key):
        try:
            return dict.__getitem__(self, key)
        except KeyError:
            return self.default

这个例子展示了几点。__init__() 方法扩展了 dict.__init__() 方法。正如 __init__() 方法通常会做的那样,它的参数列表与基类的 __init__() 方法不同。同样,__getitem__() 方法扩展了基类的 __getitem__() 方法。

__getitem__() 方法也可以如下编写,使用 Python 2.2 中引入的新的“key in dict”测试

    def __getitem__(self, key):
        if key in self:
            return dict.__getitem__(self, key)
        else:
            return self.default

我认为这个版本效率较低,因为它进行了两次键查找。例外情况是,当我们预期请求的键几乎从不在字典中时:那么设置 try/except 语句比失败的“key in self”测试更昂贵。

为了完整起见,get() 方法可能也应该扩展,使其使用与 __getitem__() 相同的默认值。

    def get(self, key, *args):
        if not args:
            args = (self.default,)
        return dict.get(self, key, *args)

(尽管此方法声明为可变长度参数列表,但它实际上应该只调用一个或两个参数;如果传递更多参数,基类方法调用将引发 TypeError 异常。)

我们不限于扩展基类上定义的方法。这是一个有用的方法,它与 update() 做类似的事情,但是保留现有值而不是在键在两个字典中都存在时用新值覆盖它们。

    def merge(self, other):
        for key in other:
            if key not in self:
                self[key] = other[key]

这使用了新的“key not in dict”测试以及新的“for key in dict:”来高效地迭代字典中的所有键(无需复制键列表)。它不要求另一个参数是 defaultdict 甚至字典:任何支持“for key in other”和 other[key] 的映射对象都可以。

这是新类型的工作方式

>>> print defaultdict               # show our type
<class '__main__.defaultdict'>
>>> print type(defaultdict)         # its metatype
<type 'type'>
>>> a = defaultdict(default=0.0)    # create an instance
>>> print a                         # show the instance
{}
>>> print type(a)                   # show its type
<class '__main__.defaultdict'>
>>> print a.__class__               # show its class
<class '__main__.defaultdict'>
>>> print type(a) is a.__class__    # its type is its class
1
>>> a[1] = 3.25                     # modify the instance
>>> print a                         # show the new value
{1: 3.25}
>>> print a[1]                      # show the new item
3.25
>>> print a[0]                      # a non-existant item
0.0
>>> a.merge({1:100, 2:200})         # use a dictionary method
>>> print a                         # show the result
{1: 3.25, 2: 200}
>>>

我们还可以在只有经典Python才允许“真实”字典的上下文中使用新类型,例如用于exec语句或内置函数eval()的locals/globals字典。

>>> print a.keys()
[1, 2]
>>> exec "x = 3; print x" in a
3
>>> print a.keys()
['__builtins__', 1, 2, 'x']
>>> print a['x']
3
>>> 

然而,我们的 __getitem__() 方法并未被解释器用于变量访问

>>> exec "print foo" in a
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "<string>", line 1, in ?
NameError: name 'foo' is not defined
>>> 

为什么这没有打印0.0?解释器使用一个内部函数来访问字典,它绕过了我们的__getitem__()覆盖。我承认这可能是一个问题(尽管它**只**在这种情况下是一个问题,即当一个字典子类被用作局部/全局字典时);是否能在不影响常见情况性能的情况下修复它,还有待观察。

现在我们将看到 defaultdict 实例具有动态实例变量,就像经典类一样。

>>> a.default = -1
>>> print a["noway"]
-1
>>> a.default = -1000
>>> print a["noway"]
-1000
>>> print a.__dict__.keys()
['default']
>>> a.x1 = 100
>>> a.x2 = 200
>>> print a.x1
100
>>> print a.__dict__.keys()
['default', 'x2', 'x1']
>>> print a.__dict__
{'default': -1000, 'x2': 200, 'x1': 100}
>>> 

这并非总是你想要的;特别是,使用单独的字典来保存单个实例变量会使 defaultdict 实例使用的内存比使用普通字典多一倍!有一种方法可以避免这种情况

class defaultdict2(dict):

    __slots__ = ['default']

    def __init__(self, default=None):
    ...(like before)...

__slots__ 声明接受一个实例变量列表,并在实例中为这些变量精确地保留空间。当使用 __slots__ 时,不能将其他实例变量赋值给它们。

>>> a = defaultdict2(default=0.0)
>>> a[1]
0.0
>>> a.default = -1
>>> a[1]
-1
>>> a.x1 = 1
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
AttributeError: 'defaultdict2' object has no attribute 'x1'
>>>

关于 __slots__ 的一些值得注意的细节和警告

  • 未定义的 slot 变量将按预期引发 AttributeError。(请注意,在 Python 2.2b2 及更早版本中,slot 变量默认值为 None,“删除”它们会恢复此默认值。)

  • 您不能使用类属性来为由 __slots__ 定义的实例变量定义默认值。__slots__ 声明会为每个槽创建一个包含描述符的类属性,将类属性设置为默认值会覆盖此描述符。

  • 没有检查来防止类中定义的槽与其基类中定义的槽之间的名称冲突。如果一个类定义了一个在基类中也定义的槽,那么由基类槽定义的实例变量将无法访问(除非直接从基类检索其描述符;这可以用于重命名它)。这样做会使您的程序含义未定义;将来可能会添加检查以防止这种情况发生。

  • 使用 __slots__ 的类的实例没有 __dict__(除非基类定义了 __dict__);但是其派生类的实例有 __dict__,除非它们的类也使用了 __slots__。

  • 您可以使用 __slots__ = [] 定义一个没有实例变量和没有 __dict__ 的对象。

  • 您不能将槽与“变长”内置类型作为基类一起使用。变长内置类型包括 long、str 和 tuple。

  • 使用 __slots__ 的类不支持对其实例的弱引用,除非 __slots__ 列表中的一个字符串等于 "__weakref__"。(在 Python 2.3 中,此功能已扩展到 "__dict__")

  • __slots__ 变量不必是列表;任何可迭代的非字符串都可以,并且迭代返回的值将用作槽名。特别是,可以使用字典。您也可以使用单个字符串来声明单个槽。然而,将来可能会为使用字典赋予额外的含义,例如,字典值可能用于限制实例变量的类型或提供文档字符串;使用非列表的东西会使您的程序含义未定义。

请注意,虽然通常运算符重载与经典类的工作方式相同,但仍有一些差异。(最大的不同是缺少对 __coerce__ 的支持;新式类应该始终使用新式数字 API,它将其他操作数未经强制转换地传递给 __add__ 和 __radd__ 方法等。)

有一种新的方式来覆盖属性访问。如果定义了 __getattr__ 钩子,它的工作方式与经典类相同:只有在常规属性搜索方式找不到属性时才会被调用。但是您现在也可以覆盖 __getattribute__,这是一个新的操作,它会为**所有**属性引用而被调用。

在覆盖 __getattribute__ 时,请记住很容易导致无限递归:每当 __getattribute__ 引用 self 的属性(即使是 self.__dict__!)时,它都会被递归调用。(这类似于 __setattr__,它会为所有属性赋值而被调用;__getattr__ 在编写不当并引用 self 的不存在属性时也可能出现此问题。)

在 __getattribute__ 内部获取 self 的任何属性的正确方法是调用基类的 __getattribute__ 方法,就像任何覆盖基类方法的子类方法可以调用基类方法一样:Base.__getattribute__(self, name)。(如果您想在多重继承的世界中保持正确,请参阅下面的 super() 讨论。)

这是一个覆盖 __getattribute__ 的例子(实际上是扩展它,因为覆盖方法调用基类方法)

class C(object):
    def __getattribute__(self, name):
        print "accessing %r.%s" % (self, name)
        return object.__getattribute__(self, name)

关于 __setattr__ 的一个注意事项:有时属性不会存储在 self.__dict__ 中(例如在使用 __slots__ 或属性时,或者在使用内置基类时)。与 __getattribute__ 相同的模式适用,您需要调用基类的 __setattr__ 来完成实际工作。这里有一个例子

class C(object):
    def __setattr__(self, name, value):
        if hasattr(self, name):
            raise AttributeError, "attributes are write-once"
        object.__setattr__(self, name, value)

C++程序员可能会发现,理解Python中这种子类型化形式的实现方式与C++中的单继承子类化非常相似是有用的,其中__class__扮演着虚函数表(vtable)的角色。

还有很多可以解释的内容(比如 __metaclass__ 声明和 __new__ 方法),但其中大部分都相当深奥。如果你感兴趣,请参阅下文

我将以一份注意事项列表结束

  • 您可以使用多重继承,但不能从不同的内置类型多重继承(例如,您不能创建一个同时继承自内置 dict 和 list 类型的类型)。这是一个永久性限制;要解除它,需要对 Python 的对象实现进行太多更改。但是,您可以通过继承自“object”来创建 mix-in 类。这是一个新的内置类型,命名了新系统下所有内置类型的功能缺失基类型。

  • 当使用多重继承时,您可以在基类列表中混合经典类和内置类型(或从内置类型派生的类型)。 (这在 Python 2.2b2 中是新增的;在早期版本中则不行。)

  • 另请参阅 2.2 错误列表

作为工厂函数的内置类型

上一节展示了内置子类型 defaultdict 的实例可以通过调用 defaultdict() 创建。这是预期的,因为这对于经典类也适用。但这里有一个新功能:内置基类型本身也可以通过直接调用类型来实例化。

对于几个内置类型,经典 Python 中已经存在以类型命名的工厂函数,例如 str() 和 int()。我已将这些内置函数更改为现在是相应类型的名称。虽然这会将这些名称的类型从内置函数更改为内置类型,但我预计这不会造成向后兼容性问题:我已确保这些类型可以用与旧函数完全相同的参数列表调用。(它们通常也可以无参数调用,生成具有合适默认值(例如零或空)的对象;这是新功能。)

这些是受影响的内置函数

  • int([number_or_string[, base_number]])
  • long([number_or_string])
  • float([number_or_string])
  • complex([number_or_string[, imag_number]])
  • str([object])
  • unicode([string[, encoding_string]])
  • tuple([iterable])
  • list([iterable])
  • type(object) 或 type(name_string, bases_tuple, methods_dict)

type() 的签名需要解释:传统上,type(x) 返回对象 x 的类型,这种用法仍然受支持。然而,type(name, bases, methods) 是一种新的用法,它创建一个全新的类型对象。(这涉及 元类编程,我在此不再深入讨论,只指出这个签名与 Don Beaudry 的元类钩子使用的签名相同。)

还有一些遵循相同模式的新内置函数。这些已在上面或将在下面描述

  • dict([mapping_or_iterable]) - 返回一个新的字典;可选参数必须是一个其项被复制的映射,或一个包含 2 元组(或长度为 2 的序列)的序列,用于给出要插入到新字典中的 (键,值) 对。
  • object([...]) - 返回一个不带任何特征的新对象;参数被忽略
  • classmethod(function) - 参见 下文
  • staticmethod(function) - 参见 下文
  • super(class_or_type[, instance]) - 参见 下文
  • property([fget[, fset[, fdel[, doc]]]]) - 参见 下文

这种改变的目的是双重的。首先,这使得在类语句中方便地将这些类型中的任何一个用作基类。其次,它使得测试特定类型变得稍微容易一些:您现在可以编写 isinstance(x, int),而不是编写 type(x) is type(0)。

这提醒了我。isinstance() 的第二个参数现在可以是一个类或类型的元组。例如,当 x 是 int 或 long(或这些类型中任何一个的子类实例)时,isinstance(x, (int, long)) 返回 true,类似地,isinstance(x, (str, unicode)) 测试两种字符串类型。我们没有对 issubclass() 进行此操作。(尚未。Python 2.3 中已对 issubclass() 进行了此操作。)

内省内置类型的实例

对于内置类型的实例(以及通常对于新式类),x.__class__ 现在与 type(x) 相同。

>>> type([])
<type 'list'>
>>> [].__class__
<type 'list'>
>>> list
<type 'list'>
>>> isinstance([], list)
1
>>> isinstance([], dict)
0
>>> isinstance([], object)
1
>>> 

在经典Python中,列表的方法名称可以通过列表对象的__methods__属性访问,效果与使用内置的dir()函数相同。

Python 2.1 (#30, Apr 18 2001, 00:47:18) 
[GCC egcs-2.91.66 19990314/Linux (egcs-1.1.2 release)] on linux2
Type "copyright", "credits" or "license" for more information.
>>> [].__methods__
['append', 'count', 'extend', 'index', 'insert', 'pop',
'remove', 'reverse', 'sort']
>>> 
>>> dir([])
['append', 'count', 'extend', 'index', 'insert', 'pop',
'remove', 'reverse', 'sort']

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

Python 2.2c1 (#803, Dec 13 2001, 23:06:05) 
[GCC egcs-2.91.66 19990314/Linux (egcs-1.1.2 release)] on linux2
Type "copyright", "credits" or "license" for more information.
>>> [].__methods__
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
AttributeError: 'list' object has no attribute '__methods__'
>>>

相反,您可以从dir()函数获取相同的信息,它提供了更多信息。

>>> dir([])
['__add__', '__class__', '__contains__', '__delattr__',
'__delitem__', '__eq__', '__ge__', '__getattribute__',
'__getitem__', '__getslice__', '__gt__', '__hash__', '__iadd__',
'__imul__', '__init__', '__le__', '__len__', '__lt__', '__mul__',
'__ne__', '__new__', '__reduce__', '__repr__', '__rmul__',
'__setattr__', '__setitem__', '__setslice__', '__str__', 'append',
'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse',
'sort']
>>>

新的 dir() 比旧的 dir() 提供更多信息:除了实例变量和常规方法的名称,它还显示了通常通过特殊符号调用的方法,例如 __iadd__ (+=)、__len__ (len)、__ne__ (!=)。

关于新的 dir() 函数的更多信息

  • dir() 对实例(经典或新式)显示实例变量以及由实例的类及其所有基类定义的方法和类属性。

  • dir() 对类(经典或新式)显示类及其所有基类的 __dict__ 的内容。它不显示由元类定义的类属性。

  • dir() 在模块上显示模块的 __dict__ 的内容。(这未改变。)

  • 不带参数的 dir() 显示调用者的局部变量。(同样,未更改。)

  • 有一个新的 C API 实现了 dir() 函数:PyObject_Dir()。

  • 还有更多细节;特别是,对于覆盖 __dict__ 或 __class__ 的对象,这些会被遵守,并且为了向后兼容,如果定义了 __members__ 和 __methods__,它们也会被遵守。

您可以将内置类型的方法用作“未绑定方法”

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

这就像使用用户定义类的未绑定方法一样——同样,它主要在子类方法内部有用,用于调用相应的基类方法。

与用户定义的类不同,你不能改变内置类型:尝试给内置类型的属性赋值会引发 TypeError,而且它们的 __dict__ 是只读代理对象。对新式用户定义类(包括内置类型的子类)取消了属性赋值的限制;但是即使是它们也具有只读的 __dict__ 代理,你必须使用属性赋值来替换或添加新式类的方法。示例会话

>>> list.append
<method 'append' of 'list' objects>
>>> list.append = list.append
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: can't set attributes of built-in/extension type 'list'
>>> list.answer = 42
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: can't set attributes of built-in/extension type 'list'
>>> list.__dict__['append']
<method 'append' of 'list' objects>
>>> list.__dict__['answer'] = 42
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: object does not support item assignment
>>> class L(list):
...     pass
... 
>>> L.append = list.append
>>> L.answer = 42
>>> L.__dict__['answer']
42
>>> L.__dict__['answer'] = 42
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
TypeError: object does not support item assignment
>>> 

对于好奇者:不允许更改内置类有两个原因。首先,这样做很容易破坏其他地方(无论是标准库还是运行时代码)所依赖的内置类型的不变量。其次,当 Python 嵌入到创建多个 Python 解释器的其他应用程序中时,内置类对象(作为静态分配的数据结构)在所有解释器之间共享;因此,在一个解释器中运行的代码可能会对另一个解释器造成破坏,这是不允许的。

静态方法和类方法

新的描述符 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__ 方法:在 Python 2.2 中,将方法绑定到实例的魔力(即使对于经典类!)是通过类中找到的对象的 __get__ 方法完成的。常规函数对象的 __get__ 方法返回一个绑定方法对象;staticmethod 对象的 __get__ 方法返回底层函数。如果一个类属性没有 __get__ 方法,它永远不会绑定到实例,换句话说,有一个默认的 __get__ 操作会返回未更改的对象;这就是简单类变量(例如数值)的处理方式。

类方法使用类似的模式来声明接收一个隐式首参数(即调用它们的**类**)的方法。这在C++或Java中没有等价物,也与Smalltalk中的类方法不完全相同,但可能服务于类似的目的。(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。但这强调了这些类方法与 元类 中定义的方法之间的区别,在元类中,对元方法的向上调用会将目标类作为显式第一个参数传递。(如果您不理解这一点,请不要担心,您不是唯一一个。:-))

属性:由get/set方法管理的属性

属性是一种实现属性的巧妙方式,其**用法**类似于属性访问,但其**实现**使用方法调用。这些有时被称为“托管属性”。在以前的Python版本中,你只能通过覆盖__getattr__和__setattr__来做到这一点;但覆盖__setattr__会显著减慢**所有**属性赋值,而覆盖__getattr__总是有点难以正确实现。属性让你轻松做到这一点,而无需覆盖__getattr__或__setattr__。

我先举一个例子。我们来定义一个类,它的属性 x 由一对方法 getx() 和 setx() 定义。

class C(object):

    def __init__(self):
        self.__x = 0

    def getx(self):
        return self.__x

    def setx(self, x):
        if x < 0: x = 0
        self.__x = x

    x = property(getx, setx)

这是一个小小的演示

>>> a = C()
>>> a.x = 10
>>> print a.x
10
>>> a.x = -10
>>> print a.x
0
>>> a.setx(12)
>>> print a.getx()
12
>>> 

完整的签名为 property(fget=None, fset=None, fdel=None, doc=None)。fget、fset 和 fdel 参数是当属性被获取、设置或删除时调用的方法。如果这三个参数中的任何一个未指定或为 None,则相应的操作将引发 AttributeError 异常。第四个参数是属性的文档字符串;可以从类中检索它,示例如下。

>>> class C(object):
...     def getx(self): return 42
...     x = property(getx, doc="hello")
... 
>>> C.x.__doc__
'hello'
>>> 

关于 property() 需要注意的事项(除第一项外,均为高级内容)

  • 属性不适用于经典类,但当你尝试时不会得到明确的错误。你的 get 方法会被调用,所以看起来它起作用了,但在属性赋值时,经典类实例只会将其值设置在其 __dict__ 中,而不会调用 property 的 set 方法,在那之后,property 的 get 方法也不会被调用。(你可以覆盖 __setattr__ 来解决这个问题,但这会非常昂贵。)

  • 就 property() 而言,它的 fget、fset 和 fdel 参数是函数,而不是方法——它们被传递给对象的显式引用作为它们的第一个参数。由于 property() 通常在类语句中使用,这是正确的(在调用 property() 时,方法确实是函数对象),但你仍然可以把它们看作方法——只要你不使用对方法做特殊处理的 元类

  • 当属性作为类属性(C.x)而不是实例属性(C().x)访问时,get 方法不会被调用。如果你想在属性作为类属性使用时覆盖其 __get__ 操作,你可以子类化 property——它本身是一种新式类型——以扩展其 __get__ 方法,或者你可以通过创建定义 __get__、__set__ 和 __delete__ 方法的新式类来从头开始定义一个描述符类型。

方法解析顺序

随着多重继承的出现,方法解析顺序(MRO)的问题也随之而来:即在查找具有给定名称的方法时,搜索类及其基类的顺序。

在经典Python中,规则由以下递归函数给出,也称为从左到右的深度优先规则。

def classic_lookup(cls, name):
    "Look up name in cls and its base classes."
    if cls.__dict__.has_key(name):
        return cls.__dict__[name]
    for base in cls.__bases__:
        try:
            return classic_lookup(base, name)
        except AttributeError:
            pass
    raise AttributeError, name

在 Python 2.2 中,我决定对新式类采用不同的查找规则。(为了向后兼容性考虑,经典类的规则保持不变;最终所有类都将是新式类,届时这种区别将消失。)我将首先尝试解释经典规则的问题所在。

当我们考虑“菱形图”时,经典规则的问题就显而易见了。用代码表示

class A:
    def save(self): ...

class B(A):
    ...

class C(A):
    def save(self): ...

class D(B, C):
    ...
或用表示子类化关系的箭头图表示(解释了名称)。
              class A:
                ^ ^  def save(self): ...
               /   \
              /     \
             /       \
            /         \
        class B     class C:
            ^         ^  def save(self): ...
             \       /
              \     /
               \   /
                \ /
              class D

箭头从子类型指向其基类型。此特定图表示B和C派生自A,D派生自B和C(因此也间接派生自A)。

假设 C 覆盖了在基类 A 中定义的方法 save()。(C.save() 可能会调用 A.save(),然后保存自己的一些状态。) B 和 D 不覆盖 save()。当我们在 D 实例上调用 save() 时,哪个方法被调用了?根据经典查找规则,调用的是 A.save(),忽略了 C.save()!

这不好。它可能会破坏 C(它的状态没有被保存),这从一开始就违背了从 C 继承的全部目的。

为什么这在经典Python中不是问题?菱形图在经典Python类层次结构中很少见到。大多数类层次结构使用单一继承,多重继承通常限于mix-in类。事实上,这里显示的问题可能正是多重继承在经典Python中不受欢迎的原因!

为什么这在新系统中会成为问题?类型层次结构顶部的“object”类型定义了许多可以被子类型有效扩展的方法,例如 __getattribute__() 和 __setattr__()。

(旁注:__getattr__() 方法并非真正的获取属性操作的实现;它是一个只有在无法通过正常方式找到属性时才会被调用的钩子。这经常被认为是缺陷——一些类设计确实需要一个对**所有**属性引用都会被调用的获取属性方法,而这个问题现在通过提供 __getattribute__() 解决了。但是这个方法必须能够以某种方式调用默认实现。最自然的方式是使默认实现作为 object.__getattribute__(self, name) 可用。)

因此,一个经典的类层次结构,例如:

        class B     class C:
            ^         ^  __setattr__()
             \       /
              \     /
               \   /
                \ /
              class D

在新系统中将变成一个菱形图

              object:
                ^ ^  __setattr__()
               /   \
              /     \
             /       \
            /         \
        class B     class C:
            ^         ^  __setattr__()
             \       /
              \     /
               \   /
                \ /
              class D

在原始图中,调用的是 C.__setattr__(),但在新系统中,如果使用经典查找规则,则会调用 object.__setattr__()!

幸运的是,有一个更好的查找规则。它有点难以解释,但在菱形图中它能做正确的事情,并且在继承图中没有菱形(即它是一个树)时,它与经典查找规则相同。

新的查找规则构建了一个按搜索顺序排列的继承图中所有类的列表。此构建在定义类时完成,以节省时间。为了解释新的查找规则,我们首先考虑经典查找规则的列表会是什么样子。请注意,在存在菱形的情况下,经典查找会多次访问某些类。例如,在上面的 ABCD 菱形图中,经典查找规则按此顺序访问类

D, B, A, C, A

请注意 A 在列表中出现了两次。第二次出现是多余的,因为任何在那里可能找到的东西在第一次出现时就已经找到了。但它仍然被访问(经典规则的递归实现不记得它已经访问过的类)。

根据新规则,列表将是

D, B, C, A

按照此顺序搜索方法将对菱形图进行正确的操作。由于列表的构建方式,在不涉及菱形的情况下,它永远不会改变搜索顺序。

所使用的确切规则将在下一节中解释(其中引用了另一篇论文以获取最细微的细节)。我在此仅指出查找规则的**单调性**这一重要属性:如果类 X 在类 D 的任何基类的查找顺序中先于类 Y,那么类 X 也将先于类 Y 在类 D 的查找顺序中。例如,由于 B 在 B 的查找列表中先于 A,它也在 D 的查找列表中先于 A;C 先于 A 也是如此。例外:如果在类 D 的基类中,有一个 X 先于 Y,另一个 Y 先于 X,则算法必须打破僵局。在这种情况下,一切都悬而未决;将来,这种情况可能会导致警告或错误。

(以前在此处描述的规则被证明不具有单调性属性。请参阅 Samuele Pedroni 在 python-dev 上发起的帖子。)

这难道不是向后不兼容吗?它不会破坏现有代码吗?如果我们更改所有类的方法解析顺序,就会。然而,在 Python 2.2 中,新的查找规则只会应用于内置类型的派生类型,这是一项新功能。没有基类的类语句会创建“经典类”,其基类本身是经典类的类语句也是如此。对于经典类,将使用经典查找规则。我们还可能提供一个工具来分析类层次结构,查找可能受方法解析顺序更改影响的方法。

顺序不一致和其他异常

(本节仅供高级读者阅读。)

任何用于确定方法解析顺序的算法都可能面临相互矛盾的要求。例如,当两个给定的基类在两个不同派生类的继承列表中以不同顺序出现,并且这些派生类又都被另一个类继承时,就会出现这种情况。这是一个例子

class A(object):
    def meth(self): return "A"
class B(object):
    def meth(self): return "B"

class X(A, B): pass
class Y(B, A): pass

class Z(X, Y): pass

如果您尝试这样做(使用 Z.__mro__,请参见 下文),您会得到 [Z, X, Y, A, B, object],这不满足上面提到的单调性要求:Y 的 MRO 是 [Y, B, A, object],这不是上述列表的子序列!事实上,在这里没有一个解决方案能同时满足 X 和 Y 的单调性要求。这被称为**顺序不一致**。在未来的版本中,我们可能会决定在某些情况下禁止这种顺序不一致,或者对此发出警告。

启发我修改 MRO 的书籍 "Putting Metaclasses to Work" 定义了当前实现的 MRO 算法,但其对算法的描述相当难以理解——我最初记录了一种不同的、朴素的算法,甚至没有意识到它并非总是计算出相同的 MRO,直到 Tim Peters 找到了一个反例。最近,Samuele Pedroni 找到了一个反例,表明朴素算法未能保持单调性,所以我不再描述它。Samuele 说服我使用一种名为 C3 的新 MRO 算法,该算法在论文 "A Monotonic Superclass Linearization for Dylan" 中描述。该算法将在 Python 2.3 中使用。C3 像书中的算法一样是单调的,但此外还保持了直接基类的顺序,而书中的算法并非总是如此。Michele Simionato 撰写的 The Python 2.3 Method Resolution Order 对 C3 在 Python 中的应用进行了非常易懂的描述。

如果顺序不一致“严重”,该书会禁止包含此类顺序不一致的类。当两个类定义至少一个同名方法时,它们之间的顺序不一致是严重的。在上面的示例中,顺序不一致是严重的。在 Python 2.2 中,我选择不检查严重的顺序不一致;但包含严重顺序不一致的程序的含义是未定义的,其效果将来可能会改变。但自从 Samuele 的反例以来,我们知道禁止顺序不一致不足以避免 Python 2.2 算法(来自书中)和 Python 2.3 算法(C3,来自 Dylan 论文)之间出现不同的结果。

协作方法和“super”

新类最酷但也可能是最不寻常的特性之一是编写“协作”类的可能性。协作类在设计时考虑了多重继承,使用我称之为“协作 super 调用”的模式。这在其他一些多重继承语言中被称为“call-next-method”,并且比 Java 或 Smalltalk 等单继承语言中的 super 调用更强大。C++ 没有这两种形式的 super 调用,而是依赖于类似于经典 Python 中使用的显式机制。(“协作方法”一词来自 “Putting Metaclasses to Work”。)

作为回顾,我们首先回顾传统的、非协作的 super 调用。当一个类 C 派生自基类 B 时,C 经常希望覆盖 B 中定义的方法 m。当 C 对 m 的定义调用 B 对 m 的定义来完成其部分工作时,就会发生“super 调用”。在 Java 中,C 中 m 的主体可以写入 super(a, b, c) 来调用 B 对 m 的定义,参数列表为 (a, b, c)。在 Python 中,C.m 写入 B.m(self, a, b, c) 来达到同样的效果。例如

class B:
    def m(self):
        print "B here"

class C(B):
    def m(self):
        print "C here"
        B.m(self)
我们说C的方法m“扩展”了B的方法m。这里的模式在单继承的情况下工作良好,但在多重继承时就失效了。让我们看看四个类,它们的继承图形成一个“菱形”(同一个图在上一节中以图形方式展示过)。
class A(object): ..
class B(A): ...
class C(A): ...
class D(B, C): ...

假设 A 定义了一个方法 m,B 和 C 都扩展了它。现在 D 该怎么做?它继承了 m 的两种实现,一种来自 B,一种来自 C。传统上,Python 只会选择第一个找到的实现,在这个例子中是 B 的定义。这不理想,因为它完全忽略了 C 的定义。为了理解忽略 C 的 m 有什么问题,假设这些类代表某种持久化容器层次结构,并考虑一个实现“将数据保存到磁盘”操作的方法。据推测,D 实例既有 B 的数据也有 C 的数据,以及 A 的数据(后者只有一个副本)。忽略 C 的 save 方法的定义意味着,当 D 实例被要求保存自身时,它只保存其数据的 A 和 B 部分,而不是由类 C 定义的数据部分!

C++ 注意到 D 继承了方法 m 的两个冲突定义,并发出错误消息。然后,D 的作者应该覆盖 m 来解决冲突。但是 D 的 m 定义应该做什么呢?它可以先调用 B 的 m,再调用 C 的 m,但是因为这两个定义都调用了从 A 继承的 m 的定义,A 的 m 最终会被调用两次!根据操作的细节,这在最好情况下是效率低下(当 m 是幂等的),在最坏情况下是错误。经典 Python 有同样的问题,只是它甚至不认为继承两个冲突的方法定义是错误:它只是简单地选择第一个。

解决这个困境的传统方法是将m的每个派生定义分成两部分:一个部分实现_m,它只保存一个类特有的数据;一个完整实现m,它调用自己的_m和基类(或多个基类)的_m。例如

class A(object):
    def m(self): "save A's data"
class B(A):
    def _m(self): "save B's data"
    def m(self):  self._m(); A.m(self)
class C(A):
    def _m(self): "save C's data"
    def m(self):  self._m(); A.m(self)
class D(B, C):
    def _m(self): "save D's data"
    def m(self):  self._m(); B._m(self); C._m(self); A.m(self)

这种模式有几个问题。首先,是额外的​​方法和调用的泛滥。但也许更重要的是,它在派生类中创建了对基类依赖图细节的不良依赖:A 的存在不能再被视为 B 和 C 的实现细节,因为类 D 需要了解它。如果,在程序的未来版本中,我们想从 B 和 C 中移除对 A 的依赖,这将同样影响像 D 这样的派生类;同样,如果我们要向 B 和 C 添加另一个基类 AA,它们的所有派生类也必须更新。

“call-next-method”模式结合新的方法解析顺序,很好地解决了这个问题。具体如下:

class A(object):
    def m(self): "save A's data"
class B(A):
    def m(self): "save B's data"; super(B, self).m()
class C(A):
    def m(self): "save C's data"; super(C, self).m()
class D(B, C):
    def m(self): "save D's data"; super(D, self).m()

请注意,super 的第一个参数始终是它所在的类;第二个参数始终是 self。另外请注意,m 的参数列表中没有重复 self。

现在,为了解释 super 的工作原理,让我们考虑这些类各自的 MRO。MRO 由 __mro__ 类属性给出

A.__mro__ == (A, object)
B.__mro__ == (B, A, object)
C.__mro__ == (C, A, object)
D.__mro__ == (D, B, C, A, object)

表达式 super(C, self).m 应该只在类 C 的方法 m 的实现内部使用。请记住,虽然 self 是 C 的实例,但 self.__class__ 可能不是 C:它可能是 C 的派生类(例如 D)。那么,表达式 super(C, self).m 会在 self.__class__.__mro__(用于创建 self 中实例的类的 MRO)中搜索 C 的出现,然后从该点**之后**开始查找方法 m 的实现。

例如,如果 self 是 C 实例,super(C, self).m 将找到 A 的 m 实现,同样,如果 self 是 B 实例,super(B, self).m 也会找到。但现在考虑一个 D 实例。在 D 的 m 中,super(D, self).m() 将找到并调用 B.m(self),因为 B 是 D.__mro__ 中定义 m 的第一个基类。现在在 B.m 中,super(B, self).m() 被调用。由于 self 是 D 实例,MRO 是 (D, B, C, A, object),并且 B 之后的类是 C。这就是 m 定义的搜索继续的地方。这找到了 C.m,它被调用,并反过来调用 super(C, self).m()。仍然使用相同的 MRO,我们看到 C 之后的类是 A,因此 A.m 被调用。这是 m 的原始定义,所以此时没有 super 调用。

请注意,同一个 super 表达式如何根据 self 的类找到不同的方法实现!这是协作 super 机制的关键。

如上所示的super调用容易出错:很容易将super调用从一个类复制粘贴到另一个类,却忘记将类名更改为目标类的名称,如果两个类属于同一个继承图,则不会检测到此错误。(您甚至可能由于错误地传入包含super调用的类的派生类的名称而导致无限递归。)如果我们不必显式命名类,那就太好了,但这需要Python解析器提供比我们目前能获得的更多帮助。我希望在未来的Python版本中通过让解析器识别super来解决这个问题。

与此同时,这里有一个你可以应用的小技巧。我们可以创建一个名为 __super 的类变量,它具有“绑定”行为。(绑定行为是 Python 2.2 中的一个新概念,但它形式化了经典 Python 中一个众所周知的概念:当通过实例上的 getattr 操作访问未绑定方法时,它会转换为绑定方法。它通过上面讨论的 __get__ 方法实现 。)这是一个简单的例子

class A:
    def m(self): "save A's data"
class B(A):
    def m(self): "save B's data"; self.__super.m()
B._B__super = super(B)
class C(A):
    def m(self): "save C's data"; self.__super.m()
C._C__super = super(C)
class D(B, C):
    def m(self): "save D's data"; self.__super.m()
D._D__super = super(D)

这个技巧的一部分在于使用名称 __super,它(通过名称修饰转换)包含类名。这确保了 self.__super 在每个类中(只要类名不同)都意味着不同的东西;不幸的是,在 Python 中**确实**有可能将基类的名称重用于派生类。这个技巧的另一部分是,内置的 super 可以用一个参数调用,然后创建一个未绑定版本,该版本可以通过后续的实例 getattr 操作进行绑定。

不幸的是,由于一些原因,这个例子仍然相当难看:super 要求传入类,但类在类语句执行完成之前不可用,所以 __super 类属性必须在类外部赋值。在类外部,名称修饰不起作用(毕竟它旨在成为一项隐私功能),因此赋值必须使用未修饰的名称。幸运的是,可以编写一个自动为其类添加 __super 属性的 元类;请参见 下面的 autosuper 元类示例

请注意,super(class, subclass) 也适用;这对于 __new__ 和其他静态方法是必需的。

示例:在Python中编写super。

为了说明新系统的强大之处,这里是一个纯 Python 中 super() 内置类的完整功能实现。这也可以通过详细阐述搜索过程来帮助澄清 super() 的语义。以下代码底部打印的语句将打印“DCBA”。

class Super(object):
    def __init__(self, type, obj=None):
        self.__type__ = type
        self.__obj__ = obj
    def __get__(self, obj, type=None):
        if self.__obj__ is None and obj is not None:
            return Super(self.__type__, obj)
        else:
            return self
    def __getattr__(self, attr):
        if isinstance(self.__obj__, self.__type__):
            starttype = self.__obj__.__class__
        else:
            starttype = self.__obj__
        mro = iter(starttype.__mro__)
        for cls in mro:
            if cls is self.__type__:
                break
        # Note: mro is an iterator, so the second loop
        # picks up where the first one left off!
        for cls in mro:
            if attr in cls.__dict__:
                x = cls.__dict__[attr]
                if hasattr(x, "__get__"):
                    x = x.__get__(self.__obj__)
                return x
        raise AttributeError, attr

class A(object):
    def m(self):
        return "A"

class B(A):
    def m(self):
        return "B" + Super(B, self).m()

class C(A):
    def m(self):
        return "C" + Super(C, self).m()

class D(C, B):
    def m(self):
        return "D" + Super(D, self).m()

print D().m() # "DCBA"

覆盖__new__方法

在子类化不可变内置类型(如数字和字符串)时,以及偶尔在其他情况下,静态方法 __new__ 会派上用场。__new__ 是实例构建的第一步,在 __init__ **之前**调用。__new__ 方法以类作为其第一个参数调用;它的职责是返回该类的一个新实例。将其与 __init__ 进行比较:__init__ 以一个实例作为其第一个参数调用,并且不返回任何东西;它的职责是初始化实例。在某些情况下,会创建一个新实例而不调用 __init__(例如当实例从 pickle 中加载时)。没有办法在不调用 __new__ 的情况下创建新实例(尽管在某些情况下您可以侥幸调用基类的 __new__)。

回想一下,您通过调用类来创建类实例。当类是新式类时,调用它时会发生以下情况。首先,调用类的 __new__ 方法,将类本身作为第一个参数传递,然后是原始调用接收到的所有(位置和关键字)参数。这会返回一个新实例。然后调用该实例的 __init__ 方法以进一步初始化它。(顺便说一句,这一切都由元类的 __call__ 方法控制。)

这是一个覆盖 __new__ 的子类示例——这是你通常使用它的方式。

>>> class inch(float):
...     "Convert from inch to meter"
...     def __new__(cls, arg=0.0):
...         return float.__new__(cls, arg*0.0254)
...
>>> print inch(12)
0.3048
>>> 

这个类不是很实用(甚至不是进行单位转换的正确方法),但它展示了如何扩展不可变类型的构造函数。如果不是 __new__,我们尝试覆盖 __init__,它将不起作用。

>>> class inch(float):
...     "THIS DOESN'T WORK!!!"
...     def __init__(self, arg=0.0):
...         float.__init__(self, arg*0.0254)
...
>>> print inch(12)
12.0
>>> 

覆盖 __init__ 的版本不起作用,因为 float 类型的 __init__ 是一个空操作:它立即返回,忽略其参数。

所有这些都是为了让不可变类型在允许子类化的同时保持其不可变性。如果 float 对象的值由其 __init__ 方法初始化,您就可以更改现有 float 对象的值!例如,这将起作用

>>> # THIS DOESN'T WORK!!!
>>> import math
>>> math.pi.__init__(3.0)
>>> print math.pi
3.0
>>>

我可以用其他方式解决这个问题,例如添加一个“已初始化”标志或只允许在子类实例上调用 __init__,但这些解决方案都不优雅。相反,我添加了 __new__,这是一个完全通用的机制,可以用于内置类和用户定义类,用于不可变对象和可变对象。

以下是关于 __new__ 的一些规则

  • __new__ 是一个静态方法。定义它时,你不需要(但可以!)使用“__new__ = staticmethod(__new__)”这句话,因为它的名称隐含了这一点(它被类构造函数特殊处理)。

  • __new__ 的第一个参数必须是一个类;其余参数是构造函数调用所见的参数。

  • 覆盖基类__new__方法的__new__方法可以调用该基类的__new__方法。基类__new__方法调用的第一个参数应该是覆盖__new__方法的类参数,而不是基类;如果你传入基类,你将得到基类的一个实例。(这实际上类似于将self传递给被覆盖的__init__调用。)

  • 除非你想玩接下来两点中描述的游戏,否则一个 __new__ 方法**必须**调用其基类的 __new__ 方法;这是创建对象实例的唯一方法。子类 __new__ 可以做两件事来影响结果对象:向基类 __new__ 传递不同的参数,以及在对象创建后修改它(例如初始化必需的实例变量)。

  • __new__ 必须返回一个对象。没有什么要求它返回一个其类参数的新对象,尽管这是一个约定。如果你返回一个现有对象,属于你的类或子类,构造函数调用仍然会调用其 __init__ 方法。如果你返回一个不同类的对象,它的 __init__ 方法将**不会**被调用。如果你忘记返回任何东西,Python 会无用地返回 None,你的调用者可能会非常困惑。

  • 对于不可变类,您的 __new__ 可以返回对具有相同值的现有对象的缓存引用;int、str 和 tuple 类型就是这样做的,用于小值。这是它们的 __init__ 不做任何事情的原因之一:缓存的对象会被反复重新初始化。(另一个原因是 __init__ 没有什么可初始化的了:__new__ 返回一个完全初始化的对象。)

  • 如果您子类化一个内置的不可变类型,并想添加一些可变状态(也许您向字符串类型添加了默认转换),最好在 __init__ 方法中初始化可变状态,并让 __new__ 保持不变。

  • 如果要更改构造函数的签名,通常需要同时重写 __new__ 和 __init__ 以接受新签名。但是,大多数内置类型会忽略它们不使用的方法的参数;特别是,不可变类型(int, long, float, complex, str, unicode, 和 tuple)具有一个虚拟的 __init__,而可变类型(dict, list, file, 以及 super, classmethod, staticmethod, 和 property)具有一个虚拟的 __new__。内置类型 'object' 具有一个虚拟的 __new__ 和一个虚拟的 __init__(其他类型继承了它们)。内置类型 'type' 在许多方面都很特殊;请参阅关于 元类 的部分。

  • (这与 __new__ 无关,但知道它很方便。)如果您子类化内置类型,实例会自动添加额外的空间以容纳 __dict__ 和 __weakrefs__。(不过,__dict__ 在您使用之前不会初始化,因此您不必担心每个创建的实例都会占用一个空字典的空间。)如果您不需要这些额外的空间,可以将“__slots__ = []”添加到您的类中。(有关 __slots__ 的更多信息,请参见 上面。)

  • 事实:__new__ 是一个静态方法,而不是类方法。我最初认为它必须是一个类方法,所以我添加了 classmethod 原语。不幸的是,对于类方法,在这种情况下向上调用不起作用,所以我不得不将其设为静态方法,并将显式类作为其第一个参数。具有讽刺意味的是,目前 Python 发布版中没有已知的类方法用法(除了测试套件中)。然而,类方法在其他地方仍然有用,例如,用于编程可继承的替代构造函数。

作为 __new__ 的另一个例子,这里有一种实现单例模式的方法。

class Singleton(object):
    def __new__(cls, *args, **kwds):
        it = cls.__dict__.get("__it__")
        if it is not None:
            return it
        cls.__it__ = it = object.__new__(cls)
        it.init(*args, **kwds)
        return it
    def init(self, *args, **kwds):
        pass

要创建一个单例类,您需要从 Singleton 派生;每个子类将拥有一个实例,无论其构造函数被调用多少次。为了进一步初始化子类实例,子类应该覆盖 'init' 而不是 __init__ —— __init__ 方法在每次调用构造函数时都会被调用。例如

>>> class MySingleton(Singleton):
...     def init(self):
...         print "calling init"
...     def __init__(self):
...         print "calling __init__"
... 
>>> x = MySingleton()
calling init
calling __init__
>>> assert x.__class__ is MySingleton
>>> y = MySingleton()
calling __init__
>>> assert x is y
>>> 

元类

过去,Python 中的元类话题曾让人毛骨悚然,甚至大脑炸裂(例如,参见 Python 1.5 中的元类)。幸运的是,在 Python 2.2 中,元类更容易理解,也更安全。

从术语上讲,元类简单地是“类的类”。任何其实例本身是类的类,都是元类。当我们谈论不是类的实例时,该实例的元类是其类的类:根据定义,x 的元类是 x.__class__.__class__。但当我们谈论类 C 时,我们通常指的是它的元类,即 C.__class__(而不是 C.__class__.__class__,那将是元元类;尽管我们不排除它们,但它们用处不大)。

内置的“type”是最常见的元类;它是所有内置类型的元类。经典类使用不同的元类:称为 types.ClassType 的类型。后者相对不那么有趣;它是一个历史遗物,需要赋予经典类其经典行为。您无法使用 x.__class__.__class__ 获取经典实例的元类;您必须使用 type(x.__class__),因为经典类不支持类上的 __class__ 属性(仅在实例上)。

当一个类语句执行时,解释器首先确定合适的元类 M,然后调用 M(name, bases, dict)。这一切都发生在类语句的**末尾**,在类的主体(其中定义了方法和类变量)已经执行之后。M 的参数是类名(取自类语句的字符串)、一个基类元组(在类语句开始时评估的表达式;如果类语句中没有指定基类,则为 ()),以及一个包含由类语句定义的方法和类变量的字典。这个调用 M(name, bases, dict) 返回的任何内容,然后被赋值给与类名对应的变量,这就是类语句的全部。

如何确定 M?

  • 如果 dict['__metaclass__'] 存在,则使用它。
  • 否则,如果至少有一个基类,则使用其元类(这会先查找 __class__ 属性,如果找不到,则使用其类型)。(在经典 Python 中,此步骤也存在,但只有在元类可调用时才执行。这被称为 Don Beaudry 钩子——愿它安息。)
  • 否则,如果存在名为 __metaclass__ 的全局变量,则使用它。
  • 否则,使用经典元类(types.ClassType)。

这里最常见的结果是 M 是 types.ClassType(创建经典类)或 'type'(创建新式类)。其他常见结果是自定义扩展类型(如 Jim Fulton 的 ExtensionClass)或 'type' 的子类型(当我们使用新式元类时)。但这里也可能出现完全出乎意料的情况:如果我们指定一个具有自定义 __class__ 属性的基类,我们可以将任何东西用作“元类”。这曾是我最初的 元类论文 中令人头疼的话题,我在此不再赘述。

总会有一个额外的细节。当您在基类列表中混合经典类和新式类时,将使用第一个新式基类的元类,而不是 types.ClassType(假设 dict['__metaclass__'] 未定义)。其效果是,当您将经典类与新式类结合时,其后代是新式类。

还有一点(我保证这是元类确定中的最后一个细节)。对于新式元类,有一个约束,即所选元类必须等于或是一个或多个基类的元类的子类。考虑一个类 C,它有两个基类 B1 和 B2。假设 M = C.__class__,M1 = B1.__class__,M2 = B2.__class__。那么我们要求 issubclass(M, M1) 和 issubclass(M, M2)。 (这是因为 B1 的方法应该能够在 self.__class__ 上调用 M1 中定义的元方法,即使 self 是 B1 的子类实例。)

元类书籍 描述了一种机制,通过 M1 和 M2 的多重继承,在必要时自动创建一个合适的元类。在 Python 2.2 中,我选择了一种更简单的方法,如果元类约束不满足则会引发异常;程序员有责任通过 __metaclass__ 类变量提供一个合适的元类。但是,如果其中一个基元类满足约束(包括显式给定的 __metaclass__,如果有的话),则找到的第一个满足约束的基元类将用作元类。

实际上,这意味着如果你有一个退化的元类层次结构,其形状像一个塔(意味着对于两个元类 M1 和 M2,issubclass(M1, M2) 或 issubclass(M2, M1) 至少有一个总是为真),你就不必担心元类约束。例如

# Metaclasses
class M1(type): ...
class M2(M1): ...
class M3(M2): ...
class M4(type): ...

# Regular classes
class C1:
    __metaclass__ = M1
class C2(C1):
    __metaclass__ = M2
class C3(C1, C2):
    __metaclass__ = M3
class D(C2, C3):
    __metaclass__ = M1
class C4:
    __metaclass__ = M4
class E(C3, C4):
    pass

对于类 C2,由于 M2 是 M1 的子类,约束得到满足。对于类 C3,由于 M3 既是 M1 又是 M2 的子类,约束得到满足。对于类 D,显式元类 M1 不是基元类(M2,M3)的子类,但选择 M3 满足约束,因此 D.__class__ 是 M3。然而,类 E 是一个错误:涉及的两个元类是 M3 和 M4,并且两者都不是彼此的子类。我们可以按如下方式修复后一种情况

# A new metaclass
class M5(M3, M4): pass

# Fixed class E
class E(C3, C4):
    __metaclass__ = M5

(元类书中描述的方法将根据类 E 的原始定义自动提供 M5 的类定义。)

元类示例

让我们先回顾一些理论。记住,类语句会调用 M(name, bases, dict),其中 M 是元类。现在,元类是一个类,我们已经确定当一个类被调用时,它的 __new__ 和 __init__ 方法会依次被调用。因此,会发生类似这样的事情

cls = M.__new__(M, name, bases, dict)
assert cls.__class__ is M
M.__init__(cls, name, bases, dict)

我在这里将 __init__ 调用写成了一个未绑定方法调用。这澄清了我们调用的是 M 中定义的 __init__,而不是 cls 中定义的 __init__(那将是 cls 实例的初始化)。但它实际上调用的是对象 cls 的 __init__ 方法;cls 恰好是一个类。

我们的第一个例子是一个元类,它遍历一个类的方法,查找名为 _get_ 和 _set_ 的方法,并自动添加名为 的属性描述符。事实证明,覆盖 __init__ 就足以实现我们想要的功能。该算法分两步进行:首先收集属性名称,然后将它们添加到类中。收集过程遍历 dict,即表示类变量和方法的字典(不包括基类变量和方法)。但第二步,属性构建过程,将 _get_ 和 _set_ 作为类属性查找。这意味着如果一个基类定义了 _get_x,而一个子类定义了 _set_x,那么子类将拥有一个由这两个方法创建的属性 x,即使只有 _set_x 出现在子类的字典中。因此,您可以在子类中扩展属性。请注意,我们使用 getattr() 的三参数形式,因此缺失的 _get_x 或 _set_x 将被转换为 None,而不是引发 AttributeError。我们还以协作方式使用 super() 调用基类 __init__ 方法。

class autoprop(type):
    def __init__(cls, name, bases, dict):
	super(autoprop, cls).__init__(name, bases, dict)
	props = {}
	for member in dict.keys():
            if member.startswith("_get_") or member.startswith("_set_"):
		props[member[5:]] = 1
	for prop in props.keys():
            fget = getattr(cls, "_get_%s" % prop, None)
            fset = getattr(cls, "_set_%s" % prop, None)
            setattr(cls, prop, property(fget, fset))

让我们用一个愚蠢的例子来测试 autoprop。这里有一个类,它将属性 x 存储为其倒置值在 self.__x 下。

class InvertedX:
    __metaclass__ = autoprop
    def _get_x(self):
        return -self.__x
    def _set_x(self, x):
        self.__x = -x

a = InvertedX()
assert not hasattr(a, "x")
a.x = 12
assert a.x == 12
assert a._InvertedX__x == -12

我们的第二个例子创建了一个名为“autosuper”的类,它将添加一个私有类变量 __super,设置为 super(cls) 的值。(回想一下上面关于 self.__super 的讨论 。)现在,__super 是一个私有名称(以双下划线开头),但我们希望它是要创建的类的私有名称,而不是 autosuper 的私有名称。因此,我们必须自己进行名称修饰,并使用 setattr() 来设置类变量。为了本例的目的,我将名称修饰简化为“在前面加上一个下划线和类名”。同样,覆盖 __init__ 就足以实现我们想要的功能,并且我们再次协作地调用基类 __init__。

class autosuper(type):
    def __init__(cls, name, bases, dict):
        super(autosuper, cls).__init__(name, bases, dict)
        setattr(cls, "_%s__super" % name, super(cls))

现在让我们用经典的菱形图来测试 autosuper

class A:
    __metaclass__ = autosuper
    def meth(self):
        return "A"
class B(A):
    def meth(self):
        return "B" + self.__super.meth()
class C(A):
    def meth(self):
        return "C" + self.__super.meth()
class D(C, B):
    def meth(self):
        return "D" + self.__super.meth()

assert D().meth() == "DCBA"

(如果定义了一个与基类同名的子类,我们的 autosuper 元类很容易被骗;它实际上应该检查这种情况,如果发生则引发错误。但这比示例所需的代码量要多,所以我将把它留给读者作为练习。)

现在我们有两个独立开发的元类,我们可以将它们组合成第三个元类,它继承自两者。

class autosuprop(autosuper, autoprop):
    pass

很简单吧?因为我们以协作方式编写了两个元类(这意味着它们的方法使用 super() 调用基类方法),所以这就是我们所需要的。让我们测试一下

class A:
    __metaclass__ = autosuprop
    def _get_x(self):
        return "A"
class B(A):
    def _get_x(self):
        return "B" + self.__super._get_x()
class C(A):
    def _get_x(self):
        return "C" + self.__super._get_x()
class D(C, B):
    def _get_x(self):
        return "D" + self.__super._get_x()

assert D().x == "DCBA"

今天就到这里。希望你的大脑没有太大的负担!

向后不兼容性

放松!上面描述的大部分功能只有在你使用类语句且基类是内置对象(或者你使用显式的 __metaclass__ 赋值)时才会被调用。

可能影响旧代码的一些事情

  • 另请参阅 2.2 错误列表

  • 内省的工作方式不同(参见 PEP 252)。特别是,现在大多数对象都有一个 __class__ 属性,而 __methods__ 和 __members__ 属性不再起作用,dir() 函数的工作方式也不同。另请参见 上面

  • 一些可视为强制转换或构造函数的内置函数现在是类型对象而非工厂函数;类型对象支持与旧工厂函数相同的行为。受影响的有:complex, float, long, int, str, tuple, list, unicode 和 type。(也有新的:dict, object, classmethod, staticmethod,但由于这些是新的内置函数,我看不出这会如何破坏旧代码。)另请参见 上面

  • 有一个非常特定(幸运的是不常见)的 bug,它过去没有被检测到,但现在被报告为错误
    class A:
        def foo(self): pass
    
    class B(A): pass
    
    class C(A):
        def foo(self):
            B.foo(self)
    
    这里,C.foo想要调用A.foo,但错误地调用了B.foo。在旧系统中,因为B没有定义foo,B.foo与A.foo相同,所以调用会成功。在新系统中,B.foo被标记为需要B实例的方法,而C不是B,所以调用失败。

  • 不保证与旧扩展的二进制兼容性。在 Python 2.2 的 Alpha 和 Beta 发布周期中,我们对此进行了严格限制。截至 2.2b1,Jim Fulton 的 ExtensionClass 正常工作(通过 Zope 2.4 的测试证明),我预计其他基于 Don Beaudry 钩子的扩展也能正常工作。尽管 PEP 253 的最终目标是淘汰 ExtensionClass,但我相信 ExtensionClass 在 Python 2.2 中仍应能工作,不应早于 Python 2.3 停止支持。

附加主题

这些话题也应该讨论

  • 描述符:__get__、__set__、__delete__
  • 可子类化内置类型的规范
  • “object”类型及其方法
  • <type 'foo'> 与 <type 'mod.foo'> 与 <class 'mod.foo'>
  • 还有什么?

参考文献