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

在 Python 2.2 中统一类型和类

在 Python 2.2 中统一类型和类

Python 版本:2.2
(有关本教程的更新版本,请参阅 Python 2.2.3

吉多·范罗苏姆

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

目录

引言

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}
    >>>

我们还可以在经典代码只允许“真实”字典的上下文中使用新类型,例如用于 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__() 重写。我承认这可能是一个问题(尽管它在这种情况下是一个问题,当 dict 子类用作 locals/globals 字典时);我是否能在不损害常见情况下性能的情况下解决这个问题还有待观察。

现在我们将看到 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__ 声明覆盖基类已经定义的实例变量。如果您这样做,基类定义的实例变量将无法访问(除了直接从基类检索其描述符;这可以用于重命名它)。这样做会使您的程序的含义未定义;将来可能会添加检查以防止这种情况。

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

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

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

  • 使用 __slots__ 的类不支持对其实例的弱引用,除非 __slots__ 列表中的一个字符串等于 "__weakref__"。(嗯,这个功能可以扩展到 "__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__ 方法),但其中大部分都非常深奥。如果你感兴趣,请参阅下文

我将以一份注意事项列表作为结尾

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

  • 使用多重继承时,您可以在基类列表中混合经典类和内置类型(或从内置类型派生的类型)。 (这是 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 元组序列,给出要插入新字典的 (key, value) 对
  • object([...]) - 返回一个没有任何功能的新对象;参数被忽略
  • classmethod(function) - 参见下文
  • staticmethod(function) - 参见下文
  • super(class_or_type[, instance]) - 参见下文
  • property([fget[, fset[, fdel[, doc]]]]) - 参见下文

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

这让我想起。isinstance() 的第二个参数现在可以是类或类型的元组。例如,isinstance(x, (int, long)) 在 x 是 int 或 long(或这些类型中任何一个的子类的实例)时返回 True,类似地,isinstance(x, (str, unicode)) 测试两种字符串中的任何一种。我们没有对 isclass() 进行此操作。

内省内置类型实例

对于内置类型的实例(以及通常对于新式类),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__ 方法返回一个绑定方法对象;静态函数对象的 __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     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__() 方法并非真正的获取属性操作的实现;它只是一个钩子,仅当通过正常方式找不到属性时才会被调用。这常常被认为是缺点——有些类设计合法地需要一个针对所有属性引用都调用的获取属性方法,现在通过提供 __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):D、B、C、A。

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

这难道不是向后不兼容吗?它不会破坏现有代码吗?会的,如果我们改变所有类的方法解析顺序。然而,在 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, B, A, object]。但是如果你在 Python 2.2 中尝试这个(使用 Z.__mro__,请参阅下文),你会得到 [Z, X, Y, A, B, object]!在未来的版本中,可能会发生两件事:Z 的 MRO 可能会改变为 [Z, X, Y, B, A, object];或者类 Z 的声明可能变为非法,因为它引入了“顺序冲突”:类 A 在 X 的继承列表中排在 B 之前,但在 Y 的继承列表中排在 B 之后。

启发我改变 MRO 的书《将元类付诸实践》定义了当前实现的 MRO 算法,但其对算法的描述很难理解——我甚至没有意识到上面的算法并非总是计算出相同的 MRO,直到Tim Peters找到了一个反例。幸运的是,反例只会在继承图中存在顺序冲突时出现。该书禁止包含此类顺序冲突的类,如果顺序冲突是“严重”的。当两个类定义至少一个同名方法时,它们之间的顺序冲突是严重的。在上面的示例中,顺序冲突是严重的。在 Python 2.2 中,我选择不检查严重的顺序冲突;但包含严重顺序冲突的程序的含义未定义,其效果将来可能会改变。

协作方法和“super”

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

作为回顾,让我们首先回顾传统的非协作 super 调用。当类 C 派生自基类 B 时,C 通常希望重写 B 中定义的方法 m。“super 调用”发生在 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 和基类(es) 的 _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 也将找到 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 解析器提供比我们目前能得到的更多帮助。我希望在未来的 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 实现的 fully functional 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
    >>> 

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

    >>> 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__ 是一个空操作:它立即返回,忽略其参数。

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

    >>> # 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__ 方法的类参数,而不是基类;如果您传入基类,您将获得基类的实例。

  • 除非您想玩接下来两点描述的游戏,否则 __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 发行版中没有已知的类方法用法(除了测试套件)。如果找不到好的用途,我甚至可能会在未来的版本中取消 classmethod!

作为 __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 的方法应该能够调用在 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_<something> 和 _set_<something> 的方法,并自动添加名为 <something> 的属性描述符。事实证明,重写 __init__ 足以实现我们想要的功能。该算法分两步:首先收集属性名称,然后将它们添加到类中。收集步骤遍历 dict,这是一个表示类变量和方法的字典(不包括基类变量和方法)。但第二步,即属性构建步骤,将 _get_<something> 和 _set_<something> 查找为类属性。这意味着如果基类定义了 _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 name in dict.keys():
                if name.startswith("_get_") or name.startswith("_set_"):
                    props[name[5:]] = 1
            for name in props.keys():
                fget = getattr(cls, "_get_%s" % name, None)
                fset = getattr(cls, "_set_%s" % name, None)
                setattr(cls, name, 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,但由于这些是新的内置函数,我不认为这会破坏旧代码。)另请参见 上文

  • 曾经有一个非常具体(幸运的是不常见)的错误,以前未被发现,但现在被报告为错误
        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'>
  • 还有什么?

参考文献