在 Python 2.2 中统一类型和类
Python 版本:2.2.3
Guido van Rossum
本文档是一个不完整的草稿。我正在征求反馈。如果您发现任何问题,请写信给我 [email protected].
目录
- 变更日志
- 介绍
- 子类化内置类型
- 内置类型作为工厂函数
- 内省内置类型的实例
- 静态方法和类方法
- 属性:由 get/set 方法定义的属性
- 方法解析顺序
- 协作方法和“super”
- 覆盖 __new__ 方法
- 元类
- 向后不兼容性
- 参考资料
变更日志
自本教程的原始 Python 2.2 版本以来的更改
- 不要通过暗示 classmethod 可能消失来吓唬人们。(2002 年 4 月 4 日)
介绍
Python 2.2 引入了“类型/类统一”的第一阶段。这是一系列针对 Python 的更改,旨在消除内置类型和用户定义类之间的大多数差异。也许最明显的是,禁止在 class 语句中使用内置类型(例如列表和字典的类型)作为基类。
这是对 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 语句的 locals/globals 字典或内置函数 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__() 覆盖。我承认这可能是一个问题(虽然它只是在这种情况下出现问题,即当 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__ 的类的实例没有 __dict__(除非基类定义了 __dict__);但是,它的派生类的实例有 __dict__,除非它们的类也使用 __slots__。
- 可以使用 __slots__ = [] 定义一个没有实例变量和 __dict__ 的对象。
- 不能将 __slots__ 与以 "可变长度" 内置类型作为基类的类一起使用。可变长度内置类型包括 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__ 方法),但大多数内容都非常深奥。如果你感兴趣,请参阅 下面。
最后,我将列出一些注意事项
- 你可以使用多重继承,但不能从不同的内置类型中进行多重继承(例如,你不能创建一个从内置 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]]]]) - 请参阅 下面
这种改变的目的是双重的。首先,这使得在类语句中方便地使用这些类型中的任何一个作为基类。其次,它使测试特定类型变得更容易:与其编写 type(x) is type(0),现在可以编写 isinstance(x, int)。
这提醒了我。isinstance() 的第二个参数现在可以是类或类型的元组。例如,isinstance(x, (int, long)) 当 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__。
我将先展示一个例子。让我们定义一个类,该类有一个由一对方法 getx() 和 setx() 定义的属性 x
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
箭头从子类型指向其基类型(s)。这个特定的图意味着 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__() 方法并不是 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 的单调性要求的解决方案。这被称为顺序差异。在未来的版本中,我们可能会决定在某些情况下禁止此类顺序差异,或对其发出警告。
这本书 "将元类投入使用",它启发我改变了 MRO,定义了当前实现的 MRO 算法,但它对算法的描述很难理解 - 我最初记录了不同的、简单的算法,甚至没有意识到它并不总是计算相同的 MRO,直到 Tim Peters 找到了一个反例。最近,Samuele Pedroni 发现了一个反例,表明简单算法无法保持单调性,因此我甚至不再描述它。Samuele 说服我使用一种名为 C3 的更新的 MRO 算法,该算法在论文 "Dylan 的单调超类线性化" 中进行了描述。该算法将用于 Python 2.3。C3 与书中的算法一样是单调的,但此外还保持了直接基类的顺序,而书中的算法并不总是这样做。Michele Simionato 撰写的 Python 2.3 方法解析顺序 对 Python 的 C3 进行了非常易于理解的描述。
如果顺序差异是“严重的”,这本书禁止包含此类顺序差异的类。当两个类定义至少一个具有相同名称的方法时,两个类之间的顺序差异是严重的。在上面的示例中,顺序差异是严重的。在 Python 2.2 中,我选择不检查严重的顺序差异;但包含严重顺序差异的程序的含义是未定义的,其效果在将来可能会改变。但自从 Samuele 的反例出现后,我们知道禁止顺序差异不足以避免 Python 2.2 算法(来自本书)和 Python 2.3 算法(C3,来自 Dylan 论文)之间的不同结果。
协作方法和“super”
新类中最酷,也许也是最不寻常的功能之一是编写“协作”类的可能性。协作类是在考虑多重继承的情况下编写的,使用我称为“协作超级调用”的模式。这在其他一些多重继承语言中被称为“调用下一个方法”,并且比在 Java 或 Smalltalk 等单继承语言中找到的超级调用更强大。C++ 既没有超级调用的形式,而是依赖于类似于经典 Python 中使用的显式机制。(术语“协作方法”来自 "将元类投入使用"。)
作为复习,让我们首先回顾一下传统的、非协作的超级调用。当类 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 的保存方法的定义意味着,当请求 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 和基类(s) 的 _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 解析器的帮助。我希望在未来的 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() 的语义。以下代码底部处的 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 是如何确定的?
- 如果字典中存在 `__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)` 中至少有一个始终为真),你无需担心元类约束。例如
# 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 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,但由于这些是新的内置函数,我看不出这会如何破坏旧代码。)另请参阅 上面。
- 有一个非常具体(而且幸运的是不常见)的错误,以前一直没有被检测到,但现在会报告为错误
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'>
- 还有什么?
参考资料
- PEP 252 - 使类型更像类
- PEP 253 - 内置类型的子类型化
- Python 1.5 中的元类 - 又名杀手级笑话
- 将元类投入使用:面向对象编程的新维度,作者:Ira R. Forman 和 Scott H. Danforth。Addison-Wesley,1999 年,ISBN 0-201-43305-2。
- Dylan 的单调超类线性化,作者:Kim Barrett、Bob Cassels、Paul Haahr、David A. Moon、Keith Playford 和 P. Tucker Withington。(OOPSLA 1996)