注意: 虽然 JavaScript 不是本网站的必要组成部分,但您与内容的互动将会受到限制。请启用 JavaScript 以获得完整体验。

Python 2.2 中统一类型和类

Python 版本: 2.2.3

Guido van Rossum

本文档是不完整的草稿。我正在征求反馈意见。如果您发现任何问题,请发送邮件至 [email protected]

目录

更改日志

自最初的 Python 2.2 版本本教程以来的更改

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

简介

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

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

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

经典类在 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}
>>>

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

>>> 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__ 的一些值得注意的细枝末节和警告

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

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

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

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

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

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

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

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

请注意,虽然通常运算符重载的工作方式与经典类相同,但还是存在一些差异。(最大的一个是不支持 __coerce__;新式类应始终使用新式数字 API,该 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__ 方法),但大多数都相当深奥。 如果您感兴趣,请参阅下面的 __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 部分。
  • staticmethod(function) - 请参阅下面的 staticmethod 部分。
  • super(class_or_type[, instance]) - 请参阅下面的 cooperation 部分。
  • property([fget[, fset[, fdel[, doc]]]]) - 请参阅下面的 property 部分。

此更改的目的有两个。首先,这使得在类语句中将这些类型中的任何一个用作基类都很方便。其次,它使测试特定类型变得更容易:与其编写 type(x) is type(0),您现在可以编写 isinstance(x, int)。

这提醒了我。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__ 中设置值,而无需调用属性的 set 方法,之后也不会调用属性的 get 方法。(您可以重写 __setattr__ 来解决此问题,但成本将非常高。)

  • 关于 property(),它的 fget、fset 和 fdel 参数是函数,而不是方法——它们会被传递一个指向对象的显式引用作为它们的第一个参数。由于 property() 通常在类语句中使用,这是正确的(在调用 property() 时,这些方法实际上是函数对象),但您仍然可以将它们视为方法——只要您没有使用对方法执行特殊操作的元类

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

方法解析顺序

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

在经典的 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 类层次结构中很少见。大多数类层次结构使用单继承,而多重继承通常仅限于混入类。事实上,这里显示的问题可能就是多重继承在经典 Python 中不受欢迎的原因!

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

(旁注:__getattr__() 方法实际上不是 get-attribute 操作的实现;它是一个钩子,只有当通过正常方式找不到属性时才会被调用。这经常被认为是缺点——某些类设计确实需要一个 get-attribute 方法,该方法为所有属性引用调用,而现在通过使 __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 也将在类 D 的查找顺序中先于类 Y。例如,由于 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 的单调性要求。这被称为顺序不一致。在未来的版本中,我们可能会决定在某些情况下禁止此类顺序不一致,或针对它们发出警告。

这本书 "Putting Metaclasses to Work" 启发我改变 MRO,定义了当前实现的 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 对 Python 的 C3 进行了非常易于理解的描述。

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

协作方法和“super”

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

作为复习,让我们首先回顾一下传统的非协作超级调用。当类 C 派生自基类 B 时,C 通常想要覆盖 B 中定义的方法 m。“超级调用”发生在 C 的 m 定义调用 B 的 m 定义来完成其某些工作时。在 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,所有它们的派生类也必须更新。

“调用下一个方法”模式与新的方法解析顺序结合,很好地解决了这个问题。如下所示:

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。另请注意,self 不会在 m 的参数列表中重复。

现在,为了解释 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 也会找到 A 的 m 实现。但是现在考虑一个 D 实例。在 D 的 m 中,super(D, self).m() 将找到并调用 B.m(self),因为 B 是 D.__mro__ 中在 D 之后定义 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 解析器获得的更多帮助。我希望通过使解析器识别 super 来在未来的 Python 版本中修复此问题。

在此期间,这是一个您可以应用的技巧。我们可以创建一个名为 __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() 的语义。以下代码底部的 print 语句打印“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__']` 未定义)。其效果是,当你跨越一个经典类和一个新式类时,后代将是一个新式类。

还有一个曲折(我保证这是元类确定中的最后一个曲折)。对于新式元类,存在一个约束,即所选择的元类等于或为基类的每个元类的子类。考虑一个具有两个基类 B1 和 B2 的类 C。假设 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)` 之一始终为 true),则不必担心元类约束。例如

# 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 版本中的 bug 列表

  • 内省机制的工作方式有所不同(请参阅 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'> vs. <type 'mod.foo'> vs. <class 'mod.foo'>
  • 还有什么?

参考