Python的metaclass

514 查看

metaclass一般译作元类,它是一个类的类(型)。简单来说,元类的实例是一个类,而这个类的类就是元类了。
也就是说,相对于类可以在运行时动态构造对象而言,元类也可以在运行时动态生成类。在C++等语言中,要得知一个对象的类是很麻烦的(MFC是用宏来实现的);而Python由于有自省功能,所以非常简单。

先不说怎么定义元类,直接看怎么用:

再来看个更好玩的例子:

这里的type接收3个参数,第一个参数是类名,第二个是父类(由于允许多重继承,所以是个元组,空元组表示父类为object),第三个参数为类的成员字典。它会返回一个新风格的type对象,这个对象实际上就是一个动态生成的类。
所以Calculator就是一个父类为object的类,并且它有add和sub这2个方法。之后我又让AdvancedCalculator继承了它,并添加了mul和div这2个方法。
输出结果:

3
7
<type ‘type’>
True

11
3
12
2
<type ‘type’>
True
True

当然,type并不是唯一可以生成类的方法,new.classobj也可以实现相同的功能,不过它返回的不是新风格的type对象,而是新风格的class对象:

结果:

3
7
<type ‘classobj’>
True

至于type对象和class对象有什么区别,似乎没必要区分。在Python中,类和类型是一致的。

我们还能继承type:

输出:

3
7
<type ‘type’>
<class ‘__main__.CalculatorType’>
True

而且type对象里也能定义其他的方法,不过这些方法的第一个参数不再是self了,而是cls。因为调用时是用生成的class的实例来调用的:

结果:

3
7
Traceback (most recent call last):
File “live.py”, line 13, in <module>
print CalculatorType.add(1, 2)
TypeError: unbound method add() must be called with CalculatorType instance as first argument (got int instance instead)

可以看到,CalculatorType里并没绑定add这个方法(因为不是通过它的实例来调用的),而在它生成的Calculator类(即CalculatorType的实例)里却有。

接着就说下如何定义metaclass了,方法很简单:
在Python 2.2之后,可以直接在类定义时对__metaclass__这个属性进行赋值。
在Python 3.0之后,可以在类的父类参数中加上metaclass参数来设置。
还有一点,metaclass是一个接受三个参数(class的名字,基类tuple,class 的属性字典)的callable对象,并且它返回一个callable对象。别忘了元类本身就是这样一个callable对象(例如type),所以可以直接设置将元类名赋值给__metaclass__属性,当然你也可以用函数对象。

但是它有什么用呢?这里就演示一下:

可以看到,Calculator完全没有定义任何方法,却可以使用它的metaclass中定义的方法。
它实际上是之前的语法的简化(语法糖),输出结果是完全相同的。
class变成了语法糖,可能是你不敢相信的,不过它就是如此,并且你应该掌握之前的语法,才能更好地理解这个过程。

至于元类和基类的不同之处,强烈建议你看《Python 中的元类编程,第 2 部分》这篇文章,我懒得写代码了。

不过说了半天,仍没说出元类究竟有什么用。实际上元类能实现的,基本上都能用其他方法实现,只是元类提供了一种更高层次的抽象,可以用于实现面向方面编程(AOP)。
先介绍下AOP。当要完成一件事时,我们需要即关注其主要任务,又关注其次要任务(例如支持多线程、数据需要加解密等)。
在一般的实现中,这2类任务是耦合的,所以当次要任务变化时,整个框架都要进行很大的调整。而AOP则是将这2类任务解耦合的一种思想。
举例来说,当我们写多线程的程序时,访问共享的对象需要加锁和解锁,这2种操作明显属于次要任务;但你写程序时,一般都是加锁、访问对象、解锁这3个动作写在一起的。如果能让访问对象时自动加锁,访问完自动解锁,而且这个操作并不写在我们的访问函数或被访问对象的方法里,那就会变得很方便了。而且假如今后的访问还需要加密解密之类的操作,我们也无需改写我们的函数或被访问对象,只需更改自动调用的那个方法即可。

要实现这个目标,Python为我们提供了2种办法:
一种是使用Decorator,装饰那个访问函数,添加加解锁的操作;
另一种是使用元类,在元类中包装访问函数。实现代码可以参考《利用metaclass实现python的aop》