统一 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,该 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__` 的作用类似于虚函数表。
还有很多内容可以解释(例如 `__metaclass__` 声明和 `__new__` 方法),但其中大部分内容都相当深奥。如果您有兴趣,请参阅下面的 __new__。
我将以一些注意事项作为结尾
- 您可以使用多重继承,但不能从不同的内置类型多重继承(例如,您不能创建既继承自内置 `dict` 类型又继承自 `list` 类型的类型)。这是一个永久性的限制;要取消这个限制,需要对 Python 的对象实现进行太多更改。但是,您可以通过继承 "object" 来创建 mix-in 类。这是一个新的内置类型,命名了新系统下所有内置类型的无特征基类型。
- 使用多重继承时,您可以在基类列表中混合经典类和内置类型(或从内置类型派生的类型)。(这是 Python 2.2b2 中的新特性;在早期版本中您无法做到这一点。)
- 另请参阅 2.2 中的错误列表。
内置类型作为工厂函数
上一节表明,可以通过调用 `defaultdict()` 来创建内置子类型 `defaultdict` 的实例。这是预期的,因为这也适用于经典类。但这是一个新特性:内置基类型本身也可以通过直接调用该类型来实例化。
对于一些内置类型,在经典的 Python 中已经存在以类型命名的工厂函数,例如 `str()` 和 `int()`。我已经更改了这些内置函数,使它们现在成为相应类型的名称。虽然这会将这些名称的类型从内置函数更改为内置类型,但我预计这不会造成向后兼容性问题:我已经确保这些类型可以使用与以前的函数完全相同的参数列表来调用。(它们通常也可以在不带参数的情况下调用,生成一个具有适当默认值的对象,例如零或空;这是一个新特性。)
这些是受影响的内置类型
- int([数字或字符串[, 基数]])
- long([数字或字符串])
- float([数字或字符串])
- complex([数字或字符串[, 虚数]])
- str([对象])
- unicode([字符串[, 编码字符串]])
- tuple([可迭代对象])
- list([可迭代对象])
- type(对象) 或 type(名称字符串, 基类元组, 方法字典)
`type()` 的签名需要解释:传统上,`type(x)` 返回对象 `x` 的类型,并且仍然支持此用法。但是,`type(name, bases, methods)` 是一种新的用法,它会创建一个全新的类型对象。(这涉及到 元类编程,我不会在这里进一步讨论,只是指出这个签名与 Don Beaudry 的著名元类钩子使用的签名相同。)
还有一些新的内置函数遵循相同的模式。这些已在上面描述或将在下面描述
- dict([映射或可迭代对象]) - 返回一个新的字典;可选参数必须是复制其项的映射,或者是一个长度为 2 的 2 元组序列,给出要插入到新字典中的(键,值)对
- object([...]) - 返回一个新的无特征对象;参数将被忽略
- classmethod(函数) - 请参阅下面的 staticmethod
- staticmethod(函数) - 请参阅下面的 staticmethod
- super(类或类型[, 实例]) - 请参阅下面的 super()
- property([fget[, fset[, fdel[, doc]]]]) - 请参阅下面的 property
此更改的目的是双重的。首先,这使得在类语句中方便地使用这些类型中的任何一个作为基类。其次,它使得测试特定类型变得更容易一些:与其编写 `type(x) is type(0)`,您现在可以编写 `isinstance(x, int)`。
这让我想起来了。`isinstance()` 的第二个参数现在可以是类或类型的元组。例如,当 `x` 是 `int` 或 `long` 时(或者当是这些类型的子类的实例时),`isinstance(x, (int, long))` 返回 true,并且类似地,`isinstance(x, (str, unicode))` 测试是否是两种字符串中的任何一种。我们没有对 `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 类层次结构中很少见。大多数类层次结构使用单继承,而多重继承通常仅限于 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):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 的继承列表中位于其之后。
书《让元类发挥作用》启发我更改 MRO,其中定义了当前实现的 MRO 算法,但是其对算法的描述非常难以理解 - 我什至没有意识到直到 Tim Peters 找到了一个反例之前,上面的算法并不总是计算相同的 MRO。幸运的是,反例只能在继承图中存在顺序不一致时发生。本书禁止包含此类顺序不一致的类,如果顺序不一致是“严重的”。当两个类至少定义一个具有相同名称的方法时,这两个类之间的顺序不一致是严重的。在上面的示例中,顺序不一致是严重的。在 Python 2.2 中,我选择不检查严重的顺序不一致;但是包含严重顺序不一致的程序的含义是未定义的,并且其效果在将来可能会更改。
协作方法和 "super"
新类最酷但也可能最不寻常的功能之一是编写“协作”类的可能性。编写协作类时要考虑多重继承,并使用我称之为“协作超级调用”的模式。这在其他一些多重继承语言中称为“call-next-method”,并且比单继承语言(如 Java 或 Smalltalk)中的超级调用更强大。C++ 既没有超级调用的形式,而是依赖于类似于经典 Python 中使用的显式机制。(“协作方法”一词来自“让元类发挥作用”。)
作为回顾,让我们首先回顾一下传统的非协作超级调用。当类 C 派生自基类 B 时,C 通常会覆盖 B 中定义的方法 m。“超级调用”发生在 C 的 m 定义调用 B 的 m 定义来完成其部分工作时。在 Java 中,C 中的 m 主体可以编写 super(a, b, c) 以使用参数列表 (a, b, c) 调用 B 的 m 定义。在 Python 中,C.m 编写 B.m(self, a, b, c) 来实现相同的效果。例如
class B: def m(self): print "B here" class C(B): def m(self): print "C here" B.m(self)我们说 C 的方法 m“扩展”了 B 的方法 m。只要我们使用单继承,此处的模式就可以很好地工作,但是它会在多重继承中崩溃。让我们看一下其继承图形成“菱形”的四个类(该图在上一节中以图形方式显示)
class A(object): .. class B(A): ... class C(A): ... class D(B, C): ...
假设 A 定义了一个方法 m,而 B 和 C 都扩展了这个方法。现在 D 该怎么做呢?它继承了 m 的两个实现,一个来自 B,一个来自 C。按照传统,Python 只是简单地选择第一个找到的实现,在本例中是来自 B 的定义。这并非理想之选,因为它完全忽略了 C 的定义。要了解忽略 C 的 m 会有什么问题,假设这些类代表某种持久化容器层次结构,并考虑一个实现“将数据保存到磁盘”操作的方法。可以推测,一个 D 实例同时拥有 B 的数据和 C 的数据,以及 A 的数据(后者的一个副本)。忽略 C 的 save 方法定义意味着,当请求 D 实例保存自身时,只会保存 A 和 B 部分的数据,而不会保存由 C 类定义的数据部分!
C++ 注意到 D 继承了方法 m 的两个冲突定义,并发出错误消息。然后,D 的作者应该重写 m 以解决冲突。但是 D 的 m 定义应该做什么呢?它可以先调用 B 的 m,然后调用 C 的 m,但是由于这两个定义都调用了从 A 继承的 m 定义,因此 A 的 m 最终会被调用两次!根据操作的细节,这充其量是一种低效率(当 m 是幂等时),最坏情况下会导致错误。经典的 Python 也有同样的问题,只不过它甚至不认为继承一个方法的两个冲突定义是错误:它只是简单地选择第一个。
解决这个难题的传统方法是将每个派生的 m 定义拆分为两部分:一个只保存一个类特有的数据的部分实现 _m,以及一个完整的实现 m,它调用自身的 _m 和基类的 _m。例如
class A(object): def m(self): "save A's data" class B(A): def _m(self): "save B's data" def m(self): self._m(); A.m(self) class C(A): def _m(self): "save C's data" def m(self): self._m(); A.m(self) class D(B, C): def _m(self): "save D's data" def m(self): self._m(); B._m(self); C._m(self); A.m(self)
这种模式存在几个问题。首先,存在额外的方法和调用的增殖。但也许更重要的是,它在派生类中创建了一个不希望有的对其基类依赖关系图细节的依赖:A 的存在不再被视为 B 和 C 的实现细节,因为 D 类需要了解它。如果在程序的未来版本中,我们想从 B 和 C 中删除对 A 的依赖,这将也会影响像 D 这样的派生类;同样,如果想在 B 和 C 中添加另一个基类 AA,则必须更新它们的所有派生类。
“调用下一个方法”模式很好地解决了这个问题,并结合了新的方法解析顺序。如下所示:
class A(object): def m(self): "save A's data" class B(A): def m(self): "save B's data"; super(B, self).m() class C(A): def m(self): "save C's data"; super(C, self).m() class D(B, C): def m(self): "save D's data"; super(D, self).m()
请注意,super 的第一个参数始终是它所在的类;第二个参数始终是 self。另请注意,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 实现的 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__ 属性(仅支持实例)。
当执行 class 语句时,解释器首先确定适当的元类 M,然后调用 M(name, bases, dict)。所有这些都发生在 class 语句的末尾,在类的正文(定义方法和类变量的地方)已经执行之后。M 的参数是类名(从 class 语句中提取的字符串)、基类元组(在 class 语句开头评估的表达式;如果 class 语句中未指定基类,则为 ())以及包含 class 语句定义的方法和类变量的字典。此调用 M(name, bases, dict) 返回的任何内容都会被赋值给与类名对应的变量,这就是 class 语句的全部内容。
如何确定 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 的类定义。)
元类示例
让我们先复习一些理论。请记住,class 语句会导致调用 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,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 hook 的扩展也可以工作。虽然 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。