级别: 初级 Michele Simionato (mis6+@pitt.edu), 博士, 物理学家,匹兹堡大学 David Mertz (mertz@gnosis.cx), 博士, 开发人员,Gnosis Software,Inc.
2003 年 11 月 01 日 Michele 和 David 在 developerWorks 上发表的第一篇关于元类编程的文章收到了很多读者反馈, 其中,有一些是来自于那些希望掌握 Python 元类的精妙之处但仍感困惑的读者。本文将重温元类的使用和它们与面向对象编程概念的关系,对比类的实例化与继承,区分类方法和元方法,以及解释并解决元类冲突。
元类及其不足
在
Python中的元类编程,第
1 部分中,我们介绍了元类的概念,展示了它们的一些能力,还示例了元类在解决类和库在运行时的动态定制等这类问题方面的应用。
事实证明那篇文章是非常受欢迎,但是在简要的篇幅中还是有些细节未阐明。根据读者的反馈和在comp.lang.python中的讨论,我们将在本文中阐述一些使用技巧。特别地,我们认为以下几点对任何希望熟练掌握元类的程序员是至关重要的:
- 用户必须理解元类编程和传统面向对象编程的不同点和相同点(单继承和多继承)。
- Python 2.2 增加了内建的函数
staticmethod()
和 classmethod()
,用于创建那些在调用时无需实例对象的方法。在某种程度上,
类方法在目的上与元类中定义的(元)方法有重叠,但是精确的相似和不同也在很多程序员的头脑中产生了混淆。
- 用户应该理解元类型冲突的原因和解决方案,当您想使用多个定制的元类时,这将非常必要。我们将解释元类
合成的概念。
实例化与继承
许多程序员会混淆元类和基类的不同。从“定义”类的表面上来区分,两者看上去是类似的。可一旦深入下去,两个概念就区别开来。
在举例之前,明确一些术语是非常有意义的。
实例
是由类“制造”的 Python 对象;类充当实例的一种模版。每一个实例只能属于一个类(但一个类可以有多个实例)。我门经常说的实例对象――或“简单实例”――是“最终的”,即意味着不能作为其他对象的模版(但它仍然可能是一个
工厂或
代表,这二者的目的有重叠)。
某些实例对象就是类本身,并且所有的类都是相应
元类的实例,甚至类也只能通过实例化机制生成。通常,类是内建的标准的元类
type
的实例;只有当我们指定元类而不是指定
type 时,才需要考虑元类编程。我们也称用来实例化对象的类为那个对象的
类型。
运行实例化想法的
正交是继承的概念。这里,一个类可以有一个或多个父亲,而不是只有一个惟一的类型。并且父亲还可以有父亲,创建一个传递的子类关系后,可以方便地利用内建的
issubclass() 函数来判断继承关系。例如,如果我们定义一些类和一个实例:
清单 1. 典型的继承层次
>>> class A(object): a1 = "A"
...
>>> class B(object): a2 = "B"
...
>>> class C(A,B): a3 = "C(A,B)"
...
>>> class D(C): a4 = "D(C)"
...
>>> d = D()
>>> d.a5 = "instance d of D"
|
然后我们可以测试这些关系:
清单 2. 测试祖先
>>> issubclass(D,C)
True
>>> issubclass(D,A)
True
>>> issubclass(A,B)
False
>>> issubclass(d,D)
[...]
TypeError: issubclass() arg 1 must be a class
|
现在,一个很有趣的问题——理解超类与元类的不同所必需的一个问题——是像
d.attr 这样的属性是如何被解析的。简单起见,我们只讨论标准的查找规则,而不是回到
.__getattr__() 。这个解决方案的第一步是在
d.__dict__ 中查找名字
attr ,如果找到了,那么就是它,但如果不是,一些有趣的事情将会发生,例如:
>>> d.__dict__, d.a5, d.a1
({'a5': 'instance d'}, 'instance d', 'A')
|
查找一个不与实例关联的属性时有一个技巧,即先在实例的类中查找,然后再在从所有的超类中查找。这一过程中父类被检查的顺序被称为这个类的
方法解析顺序,您可以利用以下这个(元)方法
.mro() 看出该顺序 (但只能是从类对象中看到):
>>> [k.__name__ for k in d.__class__.mro()]
['D', 'C', 'A', 'B', 'object']
|
换句话说,访问
d.attr 时首先是在
d.__dict__ 中,然后在
D.__dict__ ,
C.__dict__ ,
A.__dict__ ,
B.__dict__ ,最后才是在
object.__dict__ 中查找,如果在任意一个地方都未找到这个名字,就会产生一个
AttributeError 。
注意,从未在查找过程中提到过元类。
元类与祖先
这里有一个普通的继承的简单例子,我们定义了一个
Noble 基类,它有子类
Prince ,
Duke ,
Baron
等等。
清单 3. 属性继承
>>> for s in "Power Wealth Beauty".split(): exec '%s="%s"'%(s,s)
...
>>> class Noble(object): # ...in fairy tale world
... attributes = Power, Wealth, Beauty
...
>>> class Prince(Noble):
... pass
...
>>> Prince.attributes
('Power', 'Wealth', 'Beauty')
|
类
Prince 继承了
Noble 的属性,
Prince
类的实例仍然遵循上述的查找链。
清单 4. 实例中的属性
>>> charles=Prince()
>>> charles.attributes # ...remember, not the real world
('Power', 'Wealth', 'Beauty')
|
如果
Duke 碰巧有一个定制元类,那么它能够以以下方式获得一些属性:
>>> class Nobility(type): attributes = Power, Wealth, Beauty
...
>>> class Duke(object): __metaclass__ = Nobility
...
|
Duke是一个类,也是元类
Nobility 的一个实例——属性的查找过程与其他对象一致:
>>> Duke.attributes
('Power', 'Wealth', 'Beauty')
|
但是
Nobility
不是
Duke 的超类,所以这就是为什么
Duke 的
实例会找到
Nobility.attributes 的原因:
清单 5. 属性与元类
>>> Duke.mro()
[<class '__main__.Duke'>, <type 'object'>]
>>> earl = Duke()
>>> earl.attributes
[...]
AttributeError: 'Duke' object has no attribute 'attributes'
|
元类属性的可用性是不会传递的,也就是说,元类的属性是对它的实例是可用的,但是对它的实例的实例是不可用的。这正是元类与超类的主要不同。下图强调了继承与实例化的异同。
图 1. 实例化与继承
因为
earl 仍然有一个类,因而您可以间接地得到其属性:
>>> earl.__class__.attributes
|
图1对比了只涉及继承或是元类的简单情况,而不是同时涉及的情况。但有些时候,一个类 C 同时有定制元类M和基类 B:
清单 6. 结合元类与超类
>>> class M(type):
... a = 'M.a'
... x = 'M.x'
...
>>> class B(object): a = 'B.a'
...
>>> class C(B): __metaclass__=M
...
>>> c=C()
|
图形化表示为:
图 2. 超类与元类的结合
根据前面的解释,我们能够想像
C.a 将解析成
M.a 或
B.a 。当解析发生时,在查找实例化元类之前,会先遵循它的
MRO 对类进行查找。
清单 7. 解析元类与超类
>>> C.a, C.x
('B.a', 'M.x')
>>> c.a
'B.a'
>>> c.x
[...]
AttributeError: 'C' object has no attribute 'x'
|
您仍然可以使用元类来增强属性,只需在被实例化的类对象上进行设置,而不是像元类的属性那样。
清单 8. 在元类中设置属性
>>> class M(type):
... def __init__(cls, *args):
... cls.a = 'M.a'
...
>>> class C(B): __metaclass__=M
...
>>> C.a, C().a
('M.a', 'M.a')
|
类的更多魔力
对于实现像
.__new__() ,
.__init__() ,
.__str__()
等这样的特殊方法来说,实例化的约束要比继承的约束更弱这一事实很重要。我们将要讨论
.__str__() 方法,对于其他特殊方法的分析与此类似。
读者可能知道,通过覆盖其
.__str__() 方法可以改变一个类对象的打印输出形式。同样,通过覆盖它的元类的
.__str__() 方法也可以修改一个类的打印输出形式。例如:
清单 9. 定制类的打印输出形式
>>> class Printable(type):
... def __str__(cls):
... return "This is class %s" % cls.__name__
...
>>> class C(object): __metaclass__ = Printable
...
>>> print C # equivalent to print Printable.__str__(C)
This is class C
>>> c = C()
>>> print c # equivalent to print C.__str__(c)
<C object at 0x40380a6c>
|
这种情况可以用下图来表示:
图 3. 元类与魔法方法
前面的讨论表明,
Printable 类中的._str_()方法不能覆盖C中的
.__str__()
方法,C 中的
.__str__() 方法继承于
object ,因此优先级高,所以打印
c 将仍旧输出标准的结果。
如果 C 是从
Printable 而不是
object 中继承了
.__str__()
方法,可能会导致一个问题:C 实例不会拥有
.__name__ 属性并且在打印
c 时将产生一个错误。当然,您仍然可以在
C 中定义一个
.__str__() 方法来改变输出
c 的方式。
类方法与元方法
另一个常见的混淆出现在 Python 类方法与定义在元类中的类方法(最好称之为
元方法)之间。
考虑下面这个例子:
清单 10. 元方法和类方法
>>> class M(Printable):
... def mm(cls):
... return "I am a metamethod of %s" % cls.__name__
...
>>> class C(object):
... __metaclass__=M
... def cm(cls):
... return "I am a classmethod of %s" % cls.__name__
... cm=classmethod(cm)
...
>>> c=C()
|
部分混淆是由于在 Smalltalk 的术语中
C.mm 被称为“
C 的类方法”,但
Python 类方法与这种叫法不同。
元方法“mm”可以从元类或类被调用,但不是不能从实例调用。类方法既可以从类中调用也可以从实例中调用(但不存在于元类中)。
清单 11. 调用元方法
>>> print M.mm(C)
I am a metamethod of C
>>> print C.mm()
I am a metamethod of C
>>> print c.mm()
[...]
AttributeError: 'C' object has no attribute 'mm'
>>> print C.cm()
I am a classmethod of C
>>> print c.cm()
I am a classmethod of C
|
另外,元方法是由
dir(M) 而不是
dir(C) 获得的,而类方法是由
dir(C)
和
dir(c) 获得的。
您只能通过在元类上调度(像
print 这样的内建包在后台完成这一步)来调用在类 MRO 中定义的元类方法:
清单 12. 有魔力的元类方法
>>> print C.__str__()
[...]
TypeError: descriptor '__str__' of 'object' object needs an argument
>>> print M.__str__(C)
This is class C
|
注意到这种调度冲突不只局限于魔法方法是重要的。如果我们通过增加一个属性
C.mm 来改变
C ,也会有同样的问题(不管这个名字是常规的方法、类方法、静态方法还是简单的属性都不例外):
清单13. 不具魔力的元类方法
>>> C.mm=lambda self: "I am a regular method of %s" % self.__class__
>>> print C.mm()
[...]
TypeError: unbound method <lambda>() must be called with
C instance as first argument (got nothing instead)
|
冲突的元类
只要您认真地使用元类,至少会遇到一次元类/元型冲突的情况。考虑有这样两个类,
A 带有元类
M_A ,
B
带有元类
M_B ,假设我们从
A 和
B 生成
C 。问题是
C 的元类是什么呢?是
M_A 还是
M_B 呢?
正确答案是
M_C ,这是一个继承自
M_A 和
M_B 的元类,如下图所示(在本文后面的
参考资料
中,到
Putting metaclasses
to work的链接中有这方面的讨论):
图4. 避免元类冲突
然而,Python 不会自动创建
M_C ,相反,它引起一个
TypeError ,警告程序员发生了冲突:
清单14. 元类冲突
>>> class M_A(type): pass
...
>>> class M_B(type): pass
...
>>> class A(object): __metaclass__ = M_A
...
>>> class B(object): __metaclass__ = M_B
...
>>> class C(A,B): pass # Error message less specific under 2.2
[...]
TypeError: metaclass conflict: the metaclass of a derived class must
be a (non-strict) subclass of the metaclasses of all its bases
|
为 C 手工创建必需的元类,可以避免元型冲突:
清单15. 手工解决元类冲突
>>> M_AM_B = type("M_AM_B", (M_A,M_B), {})
>>> class C(A,B): __metaclass__ = M_AM_B
...
>>> type(C)
<class 'M_AM_B'>
|
当您希望越过一个类的祖先向这个类“植入”附加元类的时候,元型冲突问题的解决会变得更加复杂。另外,根据父类的元类的不同,会产生多余的元类——即在不同祖先中的相同元类和在元类之间的超类/子类关系中的相同元类。可以使用模块moconfict来以健壮和自动的方式解决这些问题(见
参考资料)。
结束语
本文讨论了一些具有警示性和比较冷僻的例子,在行为变成直觉习惯之前,使用元类需要某种程度的反复试验。但是,这些问题不是无法解决的――这篇相当短的文章涉及了大多数的陷阱。自己仔细琢磨这些例子,您发现最终会有一天,程序泛化的整个新领域都可以使用元类来达到;为了收益做一些冒险是值得的。
参考资料
作者简介  | 
|  | Michele
Simionato 是一位普通而平凡的理论物理学家,一次量子波动使他对 Python 产生了兴趣,当然,如果没有遇到 David Mertz
的话,他也不会注意到那次量子波动。现在他已被 Python 深深吸引了。他愿意让读者来判断最终结果。可以通过
mis6+@pitt.edu
与他联系,或者您也可以阅读他的
Web站点。
|
对本文的评价
|