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来执行的。

这可能有点难以理解,我们来看个例子:还是如下图的继承关系:

此图像的alt属性为空;文件名为image.png
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()方法就说到这里,有任何问题,欢迎留言