Last Updated on
新式类和旧式类
首先说明一下,在Python中有一个新式类和旧式类的问题,如下:
class A:
pass
class B(object):
pass
如上:类A就是旧式类,类B就是新式类。
区别: 旧时类的继承查找顺序是从左到右深度优先,新式类的继承是根据C3算法。新式类有MRO相关方法,旧式类没有。
PS: 旧式类是Python2.2之前的东西,但是在2.7中还有兼容。python2.2之后,就可以使用新式类了。但是Python3中所有类都是新式类,不管定义的方式如何。
所以,由于我们是基于最新的Python3,所以就没有新式类和旧式类的问题。
下面就跟我一起详细研究下Python中类的多重继承问题。
类的多重继承
Python3中,所有类都是新式类,继承顺序是根据C3算法来排序的。
每个类被创建时,便会计算出此类的继承顺序,保存在一个MRO
列表中,可以通过mro()
方法返回此列表。
看下面示例:
class A(object):
def run(self):
print('A')
class B(A):
def run(self):
print('B')
print(B.mro())
# 输出结果:
[<class '__main__.B'>, <class '__main__.A'>, <class 'object'>]
如上,类B继承A,一个简单的继承关系,通过mro()方法打印出类B的继承顺序。这个顺序就是通过C3算法得到。
对于一些简单的继承关系,我们可能直接都能想到,但是对于一些复杂一点的继承关系。如下图:
一个稍微复杂一点的继承关系,那么类G的继承顺序就不是一眼能看出来了,需要根据C3算法计算。
C3算法
算法的通用公式如下:
mro(Child(Base1,Base2)) = [ Child ] + merge( mro(Base1), mro(Base2), [ Base1, Base2] )
其中主要的操作就是merge的操作,此操作也是C3算法的核心。
举个简单的例子,比如上图中的类E,继承自类B,类C,类B和类C又继承自类A。下面我们来算一下类E的mro列表:
# object类以O表示。 mro(A) = [A, O] mro(B) = [B] + merge(mro(A), [A]) = [B] + merge([A, O], [A]) = [B, A, O] mro(C) = [C] + merge(mro(A), [A]) = [C] + merge([A, O], [A]) = [C, A, O] mro(E) = [E] + merge(mro(B), mro(C), [B, C]) = [E] + merge([B, A, O], [C, A, O], [B, C]) = [E, B] + merger([A, O], [C, A, O], [C]) = [E, B, C] + merger([A, O], [A, O]) = [E, B, C, A, O]
如上所示,通过一步步的merge合并操作,最后得到mro列表。其中merge的操作,肯定很多人看不懂,看下面merge的逻辑流程图:
Merge操作逻辑:
其中有几个名词需要明白意思:
-
列表的表头: 就是列表第一个类,如
[B, A, O]
中B
就是表头 -
列表的表尾巴: 除了表头外的所有元素就为表尾,如
[B, A, O]
中A,O
就是表尾
然后我们重新来看上面例子中的类E的mro列表计算过程:
# object类以O表示。 # 这里类A继承自object内置类,是最开始的基类。所以mro(A)直接就是[A, O],这是简单化了,实际上也可以是通过merge得出: mro(A) = [A] + merge(mro(O), [O]) = [A] + merge([O], [O]) # 因为mro(O)就等于是[O]。所以根据逻辑,取第一个列表的表头,也就是O,然后看其是否在别的列表的表尾巴,明显没有,只有两个表头O,所以O提出来,放到前面,并且删除merge中所有表头的O,所以就空了。就直接得到了结果。 = [A, O] mro(B) = [B] + merge(mro(A), [A]) = [B] + merge([A, O], [A]) # 这里也是,取第一个列表的表头A,因为A不在各个列表的表尾,所以提出来,并删除所有表头中的A,所以得出结果。 = [B, A, O] mro(C) = [C] + merge(mro(A), [A]) = [C] + merge([A, O], [A]) = [C, A, O] # 看这个复杂点的 mro(E) = [E] + merge(mro(B), mro(C), [B, C]) = [E] + merge([B, A, O], [C, A, O], [B, C]) # 到这,同样,先取第一个别表的表头B,因为B不在各个列表的表尾集合[A,O, C]中,所以提出来,并删除表头中的B,得到下面的结果: = [E, B] + merger([A, O], [C, A, O], [C]) # 然后,继续重复,取第一个列表的表头A,但是发现表头A在表尾的集合中,所以跳过此列表,取一下列表的表头C,然后C不在表尾合集中,所以C提出来,得到下面结果: = [E, B, C] + merger([A, O], [A, O]) # 同上面的逻辑,很容易得到最后结果: = [E, B, C, A, O]
看明白了吗。就是这样一个不算复杂的逻辑来计算mro列表的。当我们懂得了计算mro列表的逻辑后,自然不管类的继承有多复杂,都能弄清楚继承的顺序。
PS: 根据merge的逻辑,是有可能会导致错误,定义类错误的。需要注意我们在继承的时候的一些书写顺序。
看下面示例:
class A: pass
class B: pass
class C(A, B): pass
class D(B, A): pass
class E(C, D): pass
d = D()
# 输出结果:
TypeError: Cannot create a consistent method resolution
order (MRO) for bases A, B
以上就是一个无法计算除mro列表,导致类的定义错误。我们根据C3算法来计算一下,看看为什么错误:
mro(E) = [E] + merge(mro(C), mro(D), [C, D]) = [E] + merge([C, A, B, O], [D, B, A, O], [C, D]) = [E, C] + merge([A, B, O], [D, B, A, O], [D]) = [E, C, D] + merge([A, B, O], [B, A, O]) # 到这里,注意了,根据算法逻辑,取第一个列表的表头A,但是A在第二个表尾中,然后跳过,取第二个列表的表头B,但是B在第一个列表的表尾中,所以也要跳过,但是这已经是最后一个列表了,所以就会报错! = 抛出 ERROR
所以,问题就在D和C中对于继承的顺序写法不一致,导致后面的子类在继承时会不知道先找A还是先找B,所以报错。这在定义类时,需要特别注意一下。
OK,类的多重继承问题就说完了,只要明白了MRO列表的计算方式,再怎么复杂的继承关系,也能搞明白。
另外,开头途中的类G的mro列表,就交给大家自己去计算一下。我这里直接给结果,可以看看你计算的对不对。
mro(G) = [G, E, B, F, C, D, A, O]
super()函数
super()
函数是用于调用父类(超类)的一个方法。好处是避免直接使用父类的名字,主要用于避免多重继承问题。推荐在调用父类方法时都通过此方法,而不是父类类名。
在Python2和Python3中有点区别:Python3中可以实际使用super().xxx
代替Python2的super(Class, self).xxx
另外,super()
方法就是在调用父类方法时,时根据MRO列表来进行排序的,当出现嵌套super()
的时候也还是根据调用示例的类的MRO来执行的。
这可能有点难以理解,我们来看个例子:还是如下图的继承关系:
class A:
def run(self):
print('A')
class B(A):
def run(self):
print('B')
super().run()
class C(A):
def run(self):
print('C')
class D(A):
def run(self):
print('D')
class E(B, C):
def run(self):
print('E')
super().run()
class F(C, D):
def run(self):
print('F')
class G(E, F):
def run(self):
print('G')
super().run()
print(G.mro())
g = G()
g.run()
# 输出结果:
[<class '__main__.G'>, <class '__main__.E'>, <class '__main__.B'>, <class '__main__.F'>, <class '__main__.C'>, <class '__main__.D'>, <class '__main__.A'>, <class 'object'>]
G
E
B
F
看上面代码,在G中调用super()
方法去找父类,然后在父类E中又有super()
方法,那么根据G的MRO顺序,会去找B,然后B中又有super()
方法,这时候如果只看类B,它只继承自A,所以一般的就会认为是到A中,但实际上还是根据G的MRO顺序去找到了F中。
所以,在使用super()函数时,多承的super()找找顺序是根据最初调用的类的MRO顺序去找的,像上面的例子,如何我从类E上调用,如下:
print(E.mro())
e = E()
e.run()
# 输出结果:
[<class '__main__.E'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
E
B
C
依然是根据E的MRO列表进行调用。
比如调用B:
print(B.mro())
b = B()
b.run()
# 输出结果:
[<class '__main__.B'>, <class '__main__.A'>, <class 'object'>]
B
A
你会发现,根据调用类不同,MRO顺序不同,所以会得到不同的调用结果。这在代码编程时,需要特别注意。
另外,super()和直接通过类名调用不能混合使用,不然会出现坑,如下示例,还是同样的继承逻辑:
class A:
def run(self):
print('A')
class B(A):
def run(self):
print('B')
super().run()
class C(A):
def run(self):
print('C')
super().run()
class D(A):
def run(self):
print('D')
class E(B, C):
def run(self):
print('E')
C.run(self)
class F(C, D):
def run(self):
print('F')
class G(E, F):
def run(self):
print('G')
super().run()
print(G.mro())
g = G()
g.run()
# 输出结果:
[<class '__main__.G'>, <class '__main__.E'>, <class '__main__.B'>, <class '__main__.F'>, <class '__main__.C'>, <class '__main__.D'>, <class '__main__.A'>, <class 'object'>]
G
E
C
D
我在类E中,通过类名指定调用了父类C,然后在C中又通过super()
函数调用父类,最后得到的调用顺序是G,E,C,D。发现没有,其先是正常的走到E,在E的时候因为执行了C的调用,所以到了C,然后在C之后,它又回到了按MRO顺序查找了,但是它是根据上一个类为C,直接到了C之后的D。
那么,如果我直接在G中使用类名调用,其他使用super()
调用,是不是就会按第一个super()
方法的类的MRO
顺序来调用? 并不是! 其还是按照最开始的调用父类的类的MRO顺序来进行查找
见下面示例:
class A:
def run(self):
print('A')
class B(A):
def run(self):
print('B')
super().run()
class C(A):
def run(self):
print('C')
super().run()
class D(A):
def run(self):
print('D')
class E(B, C):
def run(self):
print('E')
super().run()
class F(C, D):
def run(self):
print('F')
super().run()
class G(E, F):
def run(self):
print('G')
E.run(self)
print(G.mro())
g = G()
g.run()
# 输出结果:
[<class '__main__.G'>, <class '__main__.E'>, <class '__main__.B'>, <class '__main__.F'>, <class '__main__.C'>, <class '__main__.D'>, <class '__main__.A'>, <class 'object'>]
G
E
B
F
C
D
结论
类的多重继承,核心是MRO继承顺序,了解C3算法,搞明白MRO列表的计算得来的方式,就不会对继承顺序感到混乱。
然后,在代码中,调用父类方法时,尽量不是将类名调用和super()
调用混合写,要么都用类名,要么都用super()
,推荐都使用super()
方法来调用父类方法。
OK,类的多重继承和super()方法就说到这里,有任何问题,欢迎留言
1