概要
本文主要从语言特性和原理上阐述Python中的继承。关于继承,主要涉及到的就是属性的读写以及函数的调用二义性问题,这里不论及关于底层的对象存储相关的原理(主要是我也不懂)。另外提到的原理只针对Python2.7,这里值得一提的是,测试用例是在Pyhon2.7.12上面编写和运行的。因为Python不同版本中存在一定差异,Python3中在继承上也做了一定的修改,但这些都不影响去理解Python中较为一般性的原理。当然,本文写的这些东西一个是方便对 Python继承有更深入的认识,另一方面是为了在开发一些框架或者稍微底层通用一点的库提供参考。在实际业务开发中最好就不用到这些了,及其容易入坑。
新式类与古典类
Python中有新式类和古典类之分,Python2.2前都是只有古典类,而在Python2.2及其以上的版本,都引进了新式类,在Python3前,新式类和古典类共同存在,而Python3之后则只保留了新式类。
新式类和古典类存在较大的差异,新式类提供了统一的对象模型,新式类和古典类有哪些差别或者新式类有哪些特征可以参考这里。新式类和古典类在创建上区别就是,新式类需要继承自一个新式类或者直接继承object来创建,而古典类则不继承任何类或者继承其他古典类来创建。它们之间存在一个显著的差异就是,在古典类中,一个实例对象x的x.__class__和type(x)是不一样的(这里不考虑手动修改对象__class__属性的情况),例如下面的代码:
古典类:
1 | class A: pass |
新式类:
1 | class A(object): pass |
也就是说,古典类都是内置类型instance的实例,于是在古典类中,两个实例对象a和b之间,type(a) == type(b)总是为True。这些就是Python中古典类和新式类的一些差异,当然,更深层次的东西涉及到的就是两种不同的对象模型了。
方法解析顺序MRO(Method Resolution Order)
在Python中讲到继承,必然就会涉及到MRO。所谓MRO,简单说就是指在Python中对象调用函数的查找顺序,根据MRO,Python就可以准确调用到正确的函数而不发生歧义。上面提到的古典类与新式类中的MRO有所不同,古典类的MRO搜索是采用自左向右的深度优先方法。而新式类则是在古典类的基础上做了一点的修改和优化,采用的是C3 MRO搜索方法。下面简单介绍一下C3 MRO算法。
C3 MRO算法
首先定义一下用的的一些符号。
$C_1C_2…C_N$:表示类列表,等同于$[C_1, C_2, …, C_N]$;
对于上没的列表来说,有:
$$head=C_1$$
$$tail=C_2…C_N$$
$$C+(C_1C_2…C_N)=CC_1C_2…C_N$$
假设有一个类$C$集成自$B_1$,$B_2$,$…$,$B_N$,现在我们需要确定类$C$的MRO类列表(也就是类$C$的线性化),可以根据以下步骤唯一确定。
记$L[C]$表示类$C$的线性化,同时又有$L[C]=L[C(B_1…B_N)]$,则 类$C$的线性化类列表,该列表由类$C$,类$C$各个父类线性化类列表的合并,以及类$C$的父类这三个部分组成。可以表示为
$$L[C]=L[C(B_1…B_N)]=C+merge(L[B_1], …, L[B_N], B_1…B_N)$$
特别的,有
$$L[object]=object$$
现在只需要确定$merge$如何进行就可以了,$merge$需要对$N+1$个列表进行操作得到一个新的列表。我们记这$N+1$个列表分别为$L_1=L[B_1]$,…,$L_N=L[B_N]$,$L_{N+1}=B_1…B_N$,同时记$head_i$为$L_i$的$head$,$tail_i$为$L_i$的$tail$。于是$merge$步骤伪代码为:
上面的伪代码仅仅表示出了C3 MRO算法的思路,具体实现可能就不是最优的了。这算法描述起来就是,$merge$多个类列表的时候,从左到右的列表中,依次取出每个列表的$head$,如果该$head$不在其余所有列表的$tail$中,则取出该$head$,然后在每个列表中都删去该$head$类(存在的话其实都是响应列表的第一个元素),然后继续按前面步骤从第一个列表重新开始再取出一个,如果$head$在其它的$tail$中存在,则取下一个列表的$head$进行同样的步骤,直到可以满足条件取出一个为止。如果最后一个列表的$head$也无法满足,则$merge$操作失败,也就是说无法完成线性化。如果每一次都能提取出一个类,则最终提取出来object后终止,从而完成$merge$操作。
另外相对于单继承,有
$$L[C(B)]=C+merge(L[B],B)=C+L[B]$$
下面举个例子,有如下继承关系的类:
现在我们要算出类A的MRO,则根据上面所述的算法,有
1 | L[O] = O |
从而得到类A的MRO类列表为ABCDEFO。下面再举一个C3 MRO算法执行失败的情况,也就是无法解析出一个MRO列表,或者说这种继承关系将会产生异常。
假如有X和Y继承自object,A继承自X和Y,B继承自Y和X,也就是继承顺序相反,而有C继承自A和B,则根据前面所述的算法,有
1 | L[X] = XO |
当上面执行到merge(XYO, YXO)时,第一个列表获取head为X,但是X在第二个列表的tail中存在,因此进而获取第二个列表的head为Y,但是Y在第一个列表的tail中存在,到此没有跟多列表执行下去了,因此算法终止,返回异常。因此这种继承关系是不太好的,实际上在Python中直接会抛出异常。
Python中可以利用类的mro()函数或者__mro__属性或取其MRO搜索列表。
上面的的例子中,用写成Python程序如下:
1 | # 正常继承关系 |
多继承与super
利用C3 MRO算法,就可以确定Python中继承关系的函数调用搜索顺序了。只要一个继承关系中能够确定MRO,那么这个继承关系就是合法的。Python支持多继承,也就是一个类可以同时继承自多个其他类。讲到多继承,那么就会讲到super。猛戳这里可以查看官网文档。下面,来了解下super是啥,能干啥用。
什么是super
对super做一下类型检查,可以看到super是一个类类型(super类型),实例化后可以得到一个super对象。如下所示:
1 | print type(super) |
从官方文档可以获悉,实例化super之后得到的是一个代理对象,所谓代理对象,就是指其代理某个对象执行一些操作,这些操作等同于被代理的对象自己亲自执行的效果。利用super提供的代理对象,可以方便地调用一个类的父类或者兄弟类的函数。此外,super仅仅提供了这样的一种机制,不一定要在类里面实例化super。例如:
1 | # 调用父类函数 |
可以看到,利用super得到的代理对象准确的调用了父类函数。因此,得了解super是如何准确调用父类函数的,两个参数有分别代表什么意思?
理解super的参数与调用规则
从官网说明文档可以知道,super的两个参数是有要求的,必须满足条件:第二参数必须是第一参数自己或者其派生类的类类型或者实例对象,可以这么记一下:super(parent, child)。其中第二参数就是super实例化后代理的对象,也就是代理child执行操作。而第一个参数就是确定了其函数调用的界线。具体的调用规则可以这么理解,假设:
- parent的MRO为[A, B, C, D, E, …]
- child(如果为实例则type(child))的MRO为[C, D, E, …]
那么,super(parent, child)调用函数的MRO为[D, E, …]。具体的就是,super是child的代理,其函数查找MRO为child(如果是实例指的就是其类型)的MRO中parent后面的部分。 这就是super调用函数的规则。例如:
1 | class A(object): |
上面例子可以看出来,super(B, b)代理了b对象,执行了A中的test函数。(可以看到A中test函数中打印的self.__class__为类B,因此可以知道实际代理了b执行了其父类的函数)
结语
以上就是关于Python中多继承的一些细节,了解清楚之后就可以设计相对比较复杂的继承关系(不过不建议,简单能解决就简单搞),利用类的MRO搜索列表,就可以很清晰知道一个对象具体调用了哪个函数了。