Python 2.2 中统一类型和类
Python 2.2 中统一类型和类
Python 版本:2.2
(有关本教程的更新版本,请参阅 Python 2.2.3)
Guido van Rossum
本文档是一个不完整的草稿。我正在征求反馈。如果您发现任何问题,请写信给我 [email protected].
目录
- 简介
- 子类化内置类型
- 内置类型作为工厂函数
- 内省内置类型的实例
- 静态方法和类方法
- 属性:由 get/set 方法定义的属性
- 方法解析顺序
- 协作方法和 "super"
- 覆盖 __new__ 方法
- 元类
- 向后不兼容性
- 参考资料
简介
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 语句的 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__ 声明覆盖基类已经定义的实例变量。如果你这样做,基类定义的实例变量将无法访问(除非直接从基类检索其描述符;这可以用来重命名它)。这样做会使程序的含义变得不明确;将来可能会添加一个检查来防止这种情况。
- 使用 __slots__ 的类的实例没有 __dict__(除非基类定义了 __dict__);但它的派生类的实例有 __dict__,除非它们的类也使用 __slots__。
- 您可以使用 __slots__ = [] 定义一个没有实例变量和 __dict__ 的对象。
- 您不能将插槽与“可变长度”内置类型作为基类一起使用。可变长度内置类型是 long、str 和 tuple。
- 使用 __slots__ 的类不支持对其实例的弱引用,除非 __slots__ 列表中的一个字符串等于 "__weakref__"。(嗯,这个功能可以扩展到 "__dict__" ...)
- __slots__ 变量不必是列表;任何可以迭代的非字符串都可以,迭代返回的值将用作插槽名称。特别是,可以使用字典。您也可以使用单个字符串来声明单个插槽。但是,将来,可能会为使用字典分配额外的含义,例如,字典值可用于限制实例变量的类型或提供文档字符串;使用非列表的效果会使程序的含义变得不明确。
请注意,虽然通常运算符重载与经典类一样工作,但有一些区别。(最大区别是缺乏对 __coerce__ 的支持;新式类应该始终使用新式数字 API,它将另一个操作数未强制转换为 __add__ 和 __radd__ 方法等。)
有一种新的覆盖属性访问的方法。如果定义了 __getattr__ 钩子,它的工作方式与经典类相同:只有在常规的属性搜索方式找不到属性时才会调用它。但是您现在也可以覆盖 __getattribute__,这是一个新的操作,它被调用用于所有属性引用。
在覆盖 __getattribute__ 时,请记住很容易导致无限递归:每当 __getattribute__ 引用 self 的属性(即使是 self.__dict__!)时,它都会被递归调用。(这类似于 __setattr__,它被调用用于所有属性赋值;__getattr__ 在不小心编写并引用 self 的不存在属性时也会出现这种情况。)
在 __getattribute__ 内部从 self 获取任何属性的正确方法是调用基类的 __getattribute__ 方法,与任何覆盖基类方法的方法一样,可以调用基类方法:Base.__getattribute__(self, name)。(如果您想在多重继承世界中保持正确,请参阅下面的 super() 的讨论。)
这是一个覆盖 __getattribute__ 的示例(实际上是扩展它,因为覆盖方法调用基类方法)
class C(object): def __getattribute__(self, name): print "accessing %r.%s" % (self, name) return object.__getattribute__(self, name)
关于 __setattr__ 的说明:有时属性不会存储在 self.__dict__ 中(例如,当使用 __slots__ 或属性,或使用内置基类时)。与 __getattribute__ 相同的模式适用,您调用基类 __setattr__ 来完成实际工作。这是一个例子
class C(object): def __setattr__(self, name, value): if hasattr(self, name): raise AttributeError, "attributes are write-once" object.__setattr__(self, name, value)
C++ 程序员可能会发现,Python 中这种形式的子类型化与 C++ 中的单继承子类化非常相似,__class__ 扮演着 vtable 的角色。
还有很多内容可以解释(比如 __metaclass__ 声明和 __new__ 方法),但大多数内容都很深奥。如果您感兴趣,请参阅下面的 内容。
我将以一些注意事项结束
- 您可以使用多重继承,但不能从不同的内置类型中进行多重继承(例如,您不能创建一个从内置 dict 和 list 类型都继承的类型)。这是一个永久限制;要解除它需要对 Python 的对象实现进行太多更改。但是,您可以通过从“object”继承来创建混合类。这是一个新的内置类型,命名了新系统下所有内置类型的无特征基类型。
- 在使用多重继承时,您可以在基类列表中混合经典类和内置类型(或从内置类型派生的类型)。(这是 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)) 测试两种类型的字符串。我们没有对 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__ 方法返回一个绑定方法对象;staticfunction 对象的 __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 类层次结构中很少发现菱形图。大多数类层次结构使用单继承,多重继承通常仅限于 mix-in 类。事实上,这里显示的问题可能是多重继承在经典 Python 中不受欢迎的原因!
为什么这在新系统中会成为问题?类型层次结构顶部的“object”类型定义了许多方法,这些方法可以被子类型有益地扩展,例如 __getattribute__() 和 __setattr__()。
(旁白:__getattr__() 方法并不是获取属性操作的实际实现;它是一个钩子,只有在无法通过正常方式找到属性时才会被调用。这经常被认为是一个缺点——一些类设计对获取属性方法有合法需求,该方法被调用以获取所有属性引用,而这个问题现在通过使 __getattribute__() 可用而得到解决。但随后此方法必须能够以某种方式调用默认实现。最自然的方法是使默认实现作为 object.__getattribute__(self, name) 可用。)
因此,像这样的经典类层次结构
class B class C: ^ ^ __setattr__() \ / \ / \ / \ / class D
在新系统下将变为菱形图
object: ^ ^ __setattr__() / \ / \ / \ / \ class B class C: ^ ^ __setattr__() \ / \ / \ / \ / class D
而在原始图中调用 C.__setattr__(),在新系统下使用经典查找规则,将调用 object.__setattr__()!
幸运的是,有一个更好的查找规则。它有点难以解释,但在菱形图中它做了正确的事情,并且当继承图中没有菱形(当它是一棵树时)时,它与经典查找规则相同。
新的查找规则在继承图中构造一个所有类的列表,按照它们将被搜索的顺序。这种构造是在定义类时完成的,以节省时间。为了解释新的查找规则,让我们首先考虑经典查找规则的列表将是什么样子。请注意,在存在菱形的情况下,经典查找会多次访问某些类。例如,在上面的 ABCD 菱形图中,经典查找规则按以下顺序访问类
D, B, A, C, A
注意 A 在列表中出现了两次。第二次出现是多余的,因为在那里可以找到的任何东西在搜索第一次出现时就已经找到了。但它仍然被访问(经典规则的递归实现不记得它已经访问了哪些类)。
我们使用此观察结果来解释我们的新查找规则。使用经典查找规则,构造将被搜索的类的列表,包括重复项。现在,对于列表中多次出现的每个类,删除除最后一个之外的所有出现。生成的列表包含每个祖先类恰好一次(包括最派生的类,在本例中为 D):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 之后。
这本书 "Putting Metaclasses to Work" 启发我改变了 MRO,它定义了当前实现的 MRO 算法,但它对算法的描述很难理解 - 直到 Tim Peters 找到一个反例,我才意识到上面的算法并不总是计算相同的 MRO。幸运的是,反例只会在继承图中存在顺序不一致时才会发生。如果顺序不一致是“严重的”,这本书禁止包含此类顺序不一致的类。当两个类定义至少一个具有相同名称的方法时,两个类之间的顺序不一致是严重的。在上面的示例中,顺序不一致是严重的。在 Python 2.2 中,我选择不检查严重的顺序不一致;但包含严重顺序不一致的程序的含义是未定义的,其效果在将来可能会发生变化。
协作方法和 "super"
新类最酷的特性之一,也许也是最不寻常的特性之一,是编写“协作”类的可能性。协作类是在考虑多重继承的情况下编写的,使用一种我称为“协作超级调用”的模式。这在一些其他多重继承语言中被称为“调用下一个方法”,并且比在 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 的保存方法的定义意味着,当请求一个 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__ 方法的类参数,而不是基类;如果您要传递基类,您将获得基类的实例。
- 除非您想玩下一条子弹中描述的游戏,否则 __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 的方法应该能够在 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。我们还以协作方式调用基类的 __init__ 方法,使用 super()。
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'>
- 还有什么?
参考资料
- PEP 252 - 使类型看起来更像类
- PEP 253 - 内置类型的子类型化
- Python 1.5 中的元类 - 又名杀手级笑话
- 让元类发挥作用:面向对象编程的新维度,作者:Ira R. Forman 和 Scott H. Danforth。Addison-Wesley,1999 年,ISBN 0-201-43305-2。