8.3.4 多重继承
一个派生类有两个或多个基类称为多重继承。可从两个或多个基类中继承所需的属性和方法。如有些水果既是水果又是蔬菜,如西红柿、黄瓜等。归属关系如图8-5所示。
图8-5 蔬菜、水果分类图
派生类西红柿既具有水果的一些特点,也具有蔬菜的一些特点,相当于有两个基类,这种关系称为多重继承。现实生活中多重继承的案例有很多,如儿子的基类可以是父亲,也可以是母亲。当一个类继承了多个基类时,具有以下特点:
①拥有全部基类的属性和方法;
②子类可以作为任一父类使用。
当派生类从多个基类派生,而又没有自己的构造函数时,如何来判断它继承哪一个基类的构造方法?一般遵循以下原则:
①按顺序继承,哪个基类在最前面且它又有自己的构造函数,就继承它的构造函数;
②如果第1个基类没有构造函数,则继承第2个基类的构造函数,第2个也没有,就再往后找,以此类推。
此原则同样适用于不同基类中的同名方法。
西红柿的继承关系用代码表示如下:
由于类Tomato没有自己的构造方法,同时有两个基类,其中基类Vegetable在前,故继承的会是Vegetable的构造方法。运行结果如下:
这种从同一种基类派生出来的继承方式是按照广度优先顺序来进行的。继承顺序如图8-6所示。
广度优先遍历是指从Tomato开始往上搜索到Vegetable,若Vegetable没有数据,则搜索和Vegetable同级的Fruit里的数据,若同级的Fruit里还是没有数据,再继续往上搜索,直到Plant。而如果Tomato在往上的基类Vegetable中找到数据,则继承Vegetable的数据。因此上例代码的输出结果只有“这是蔬菜味道”。
图8-6 交叉继承广度优先继承顺序
如果Vegetable和Fruit不是从同一基类派生出来的,则会采用深度优先的继承方式,继承顺序如图8-7所示。
图8-7 正常继承广度优先继承顺序
假设Vegetable和Fruit不是从同一基类派生出来的,代码如下:
由于Vegetable和Fruit拥有不同的基类,所以继承顺序采用深度优先,深度优先遍历是指从Tomato开始往上搜索到Vegetable,若Vegetable没有数据,则搜索Vegetable的基类PlantS里的数据,若基类PlantS里还是没有数据,再继续往上搜索,但往上基类是object,则停止该方向的深度遍历,进行Tomato基类的广度优先遍历,找到Fruit,在这个方向继续进行深度优先遍历,如果Fruit里没有数据,则会继续往上找到PlantH。这段代码输出的结果是:
如果派生类也定义了自己的构造函数,同时调用了多个基类的构造函数,那么顺序为:按继承顺序,哪个在前就先调用哪一个基类的构造函数。
例如,给西红柿类创建构造方法,同时调用父类的构造方法,代码如下:
运行结果为:
从运算结果可以看出,共同的基类Plant的构造方法被调用了两次,这个是因为父类名调用构造方法时,由于Vegetable和Fruit都继承自Plant,每一次都会调用访问父类Plant。想要优化这个情况,我们可以将调用父类的方法格式改成super()函数。
使用super()函数的好处在于,如果要改变子类继承的父类(由A改为B),需要修改一行代码,比如要将class Vegetable(PlantS)的父类由PlantS改为Plant,需要对父类调用方法的语句逐一进行修改,若是采用super()函数,只需要修改class Vegetable(Plant)即可,而不需要在class Vegetable的大量代码中去查找、修改基类名,此外代码的可移植性和重用性也更高。
用super()方法改写不同基类的代码如下:
运行结果:
从运行结果可以看出,当基类不同时,遵循的继承调用顺序是Tomota→Vegetable→PlantS→Fruit→PlantH→object。Tomota先进入自身的__init__()方法,发现调用父类构造方法语句super().__init__(),则进入父类Vegetable的__init__()方法,在Vegetable类中又存在父类构造方法的调用,则进入PlantS类的构造方法。然后按顺序依次执行对应构造方法的输出语句。
用super()方法改写相同基类的代码如下:
运行结果:
从运行结果可以看出,当基类相同时,按照广度优先的顺序依次继承。从自身的__init__()方法进入,遇到super().__init__(),按照广度优先顺序,进入直接基类Vegetable(),再进入类Fruit(),这两个类的__init__()方法又调用其共同基类super().__init__(),即Plant的__init__()方法,因而输出顺序是“这是植物”“这是水果味道”“这是蔬菜味道”。共同基类plant的__init__()方法只会被执行一次。
由此可知super()是用来解决多重继承问题的,直接用类名调用父类方法在使用单继承的时候没问题,但是如果使用多重继承,会涉及查找顺序(MRO)、重复调用(钻石继承)等问题。
注意:super()的本质不是父类,而是执行MRO中的下一个类!
MRO(Method Resolution Order)是类的方法解析顺序表,其实也是继承父类方法的顺序表。按不同的继承方式,MRO查找顺序不同,具体示意分别如图8-8、图8-9所示。
图8-8 正常继承方式继承顺序
图8-9 交叉继承方式继承顺序