写在前面
这两天仔细研究了python中元类的概念,从最开始的一头雾水,到现在的渐渐有一点明白。想借这篇文章来阐述一下我对于python中元类的一些粗浅见解,同时也希望能给其他人一些启发,共同进步。首先,我想说在理解元类时一定要在大脑中时刻按照OOP的编程理念对程序进行分析才能够理解元类中一些比较难以理解的地方,这也是笔者学习元类的一个小经验。
什么是元类
元类在python中最简单的定义就是——type的子类。为了弄清楚这个模糊的定义,首先要弄清楚的是什么是type类,这对于后面的理解是非常重要的。type顾名思义就是“类型”的意思,它和python标准库中的其他类一样也是一个类,但是它也有非常特殊的地方。其他的类的实例就是一个普通的实例,而类型的实例是一个类。这是python3.0中一个更新的点,在2.X版本的python中类型的实例还不完全是一个类,在某些场景下它的实例也是一个普通的实例,不过今天我们要谈的不是2.X版本中的type。让我们用几句简单的代码来验证一下:
在代码中首先构造了一个list实例,然后用type的构造函数以list的实例为参数构造了一个type的实例,打印后我们发现type的实例是一个类,这和list实例的类(l.__class__)是相同的。这就印证了我们的一个论断——类型的实例就是类。
熟悉OOP的人特别是C++的人都应该了解,类只是声明而不是实例,只有在程序中实例化一个类才会分配内存。但是python中类也是一个叫做类对象的对象,它不是凭空出现在程序中的,它和实例一样也需要有代码对类对象进行实例化,说穿了类对象应该也是一种特殊的实例,为什么这么说呢?图一中的代码应该能给我们些许暗示:通过type的构造函数构造出类型的实例是一个类,或者说类对象(不能再细说了,再说就绕进去了,读者自己意会吧)
那么,写到这里我们就可以说type类负责类对象的创建,一般情况下这种创建是隐式的不被我们发觉的。而如果我们要显式地观察这个过程就可以通过创建元类来实现。在OOP中拦截一个类方法的方式之一就是继承这个类并且重载需要拦截的方法,那么这里就可以引出最开始给出的元类定义,元类就是type的子类。元类通过继承type类,进而重载type类中的一些方法来达到控制类对象生成的目的,这是元类编程中一个大体的思想。
类对象的创建过程
类对象的创建过程和实例的创建过程相似或者说大体上是一致的,我们通过一个例子来了解吧。
打印效果如下:
可见在类对象创建时首先调用元类中的__new__方法,然后调用__init__方法。其中__new__方法返回类对象的实例,__init__方法对类对象进行一些初始化。这两个方法都是type类中的方法,在这里我们继承type类后重载这两个方法等于覆盖了type类中的这两个方法。有了这个直观的感受我们接着进一步探索类对象的创建过程。
一个类在声明了元类之后(就是类名后面加个括号,里面写着metaclass=XXX)。当程序运行时,在class语句的末尾就会自动创建类对象。假设我们有一个demo类,声明它的元类为Meta,那么在demo的class语句完结后紧接着执行一句:
demo=Meta(name,bases,dict)
传入三个参数,第一个是demo的类名称(字符串类型),第二个是demo类的父类元组,第三个是demo类的类字典。这时候就需要关注Meta了,Meta也是一个类,它是type的子类,同时type也是Meta的元类,就是说Meta类对象是type的实例。在type类中有一个__call__方法,这个方法是一个运算符重载方法,拦截type(xxx,xxx,xxx,...)这样的调用。回到刚才的
demo=Meta(name,bases,dict)
由于Meta是type的实例,因此当这样的调用形式出现时必然会触发type中的__call__方法。由于Meta是type的实例,因此在传参的时候除了刚刚写出的三个参数外还会自动传入一个Meta自己,因此type的__call__方法实际上会接收到四个参数。现在程序运行到了type的__call__方法中,在这个方法中的调用过程我们用这样的一段伪代码来展示:
正如同代码中所展示的,在__call__方法中首先调用元类的__new__方法得到一个类对象,再把这个类对象传入元类的__init__方法对这个类对象进行初始化最后再返回这个类对象,这也印证了最初我们的论断。元类构造一个类对象基本上就是这么一个过程。
一个例子
为了更加说明元类构造类对象的过程,我从书上找了一个例子改了一下贴在这:
这个例子是为了说明元类构建类对象时__call__方法的调用。首先梳理一下程序的结构:Eggs和Spam是两个常规的类,其中Spam是Eggs的子类,Spam的元类为SubMeta,而SubMeta的元类又为SuperMeta。之所以要绕这么一下就是为了让元类本身的构造过程也暴露出来。
接下来分析一下程序的运行。首先要构建Spam就要先构建SubMeta。SubMeta的元类为SuperMeta,在SuperMeta中定义了__init__和__new__方法,这是定义元类中一个比较常规的做法就不说了,我们要关注一下SuperMeta中定义的__call__方法并关注这个方法执行的时机,这很重要。首先,构建SubMeta等于执行这样的语句:
SubMeta=SuperMeta(name,bases,dict)
但这是否意味着SuperMeta的__call__会执行呢?答案是否定的,因为从原理上来说SuperMeta是type的实例,因此上面的调用会执行type中的__call__方法而不是SuperMeta中的。接着,由于SuperMeta中重载了type的__new__和__init__方法,因此type类中的__call__会调用SuperMeta的这两个方法,调用之后SubMeta类对象就构建完成了,注意此时的SubMeta是SuperMeta的类实例,明白这点很重要。接下来就要构建Spam类对象:
Spam=SubMeta(name,bases,dict)
由于SubMeta是SuperMeta的实例,因此上面代码的调用会触发SuperMeta的__call__方法,就是我们刚刚提到的那个。接着会调用SubMeta类中的__init__和__new__方法,如果SubMeta中这两个方法找不到或者没找全,程序就会顺着继承树找type类中的对应方法。
那么我们预测一下输出吧,首先肯定是SuperMeta的__new__和__init__执行,然后是SuperMeta的__call__执行,接着是SubMeta的__new__和__init__执行:
结果很显然,印证了之前的预测。
在这里,我们还可以继续思考一下。此处的Spam是SubMeta的实例,如果在SubMeta中定义一个__call__方法,那么当Spam正常创建实例的时候会发生什么呢?这个问题就暗示了python创建实例背后的故事,其实这个过程和类对象的构建过程非常相似甚至代码都是一模一样的,同样也是触发__call__方法,进而调用__new__分配内存,然后调用__init__做一些初始化的工作。只不过和类对象不同的是类对象创建中__new__返回的是一个类,而实例创建中返回的是一个实例。之所以会有这样的区别在于在__call__方法中__new__和__init__的调用是取决于__call__的第一个参数,上面的例子中即为Spam,而Spam不是type的子类,因此Spam的__init__和__new__方法指向的是他自己或者是object的对应方法,因此才出现了__new__方法返回结果不同的现象。
写在最后
好啦,元类基本的东西差不多就是这样了,至于具体应用还是蛮多的比如给类动态添加方法,模拟实现java中接口的特性等等。这都需要自己慢慢探索实践。