Skip to content

多继承基础 ​

学习层次

本章属于拓展回查。首次学习先掌握多继承语法、同名方法冲突和 __mro__ 的查看方式;复杂的协作式初始化不作为基础阶段的独立编写要求。

上一节学习的单继承中,一个子类只直接继承一个父类。但有些对象需要同时获得多个类提供的能力。一个对象什么时候适合拥有多个父类?父类顺序变化后,方法调用结果会不会变化?

例如,狮虎兽既需要老虎的技能,也需要狮子的技能。如果只继承其中一个类,另一部分能力就无法直接使用。这时可以让一个子类同时继承多个父类,这就是多继承。本节先掌握语法、同名方法查找和初始化边界,再判断多继承是否适合当前关系。

多继承适用场景 ​

继续使用动物场景。现在要定义一个狮虎兽类,它需要同时获得老虎和狮子的能力:

text
狮虎兽(Liger)
    公共属性:name、high、weight
    公共方法:eat
    老虎提供:bark、tiger_skill
    狮子提供:bark、lion_skill

如果完全不使用继承,Liger 就要再次编写公共属性、eat() 和两类动物的技能:

无继承时的类结构:代码重复与冗余维护痛点

这些代码虽然可以运行,但是重复内容较多。使用多继承后,Liger 可以同时获得 Tiger 和 Lion 提供的能力:

使用多继承后的类结构:能力组合与代码完全复用

多继承语法 ​

定义子类时,在括号中依次写出多个父类:

python
class 子类名(父类名1, 父类名2):
    pass

例如:

python
class Liger(Lion, Tiger):
    pass

Liger 同时继承 Lion 和 Tiger。括号中父类的先后顺序不是随便写的,它会影响同名方法的查找结果。

先让案例运行起来,再观察父类顺序的具体影响。

案例:狮虎兽多继承实现 ​

需求:定义 Liger,让它同时获得 Lion 和 Tiger 的能力,并观察多个父类提供的方法如何协作。

功能分析:先定义两个共享 Animal 的父类,再创建 Liger 实例,最后通过同名方法调用观察父类顺序的影响。

父类定义 ​

Animal 保存三种动物共有的属性和 eat()。Tiger、Lion 分别继承 Animal,再增加各自的叫声和技能:

python
class Animal:
    def __init__(self, name, high, weight):
        self.name = name
        self.high = high
        self.weight = weight

    def eat(self):
        print(f'{self.name} 正在吃东西')


class Tiger(Animal):
    def bark(self):
        print(f'{self.name} 正在嗷呜叫')

    def tiger_skill(self):
        print(f'{self.name} 使用老虎技能')


class Lion(Animal):
    def bark(self):
        print(f'{self.name} 正在吼叫')

    def lion_skill(self):
        print(f'{self.name} 使用狮子技能')

这里暂时不给 Tiger 和 Lion 单独编写 __init__()。它们会继续使用 Animal.__init__(),让案例先聚焦多继承和方法查找顺序。

多父类继承 ​

接着定义 Liger,同时继承 Lion 和 Tiger:

python
class Liger(Lion, Tiger):
    pass


liger = Liger('狮虎兽壹号', 1.3, 60)
liger.eat()
liger.tiger_skill()
liger.lion_skill()

运行结果:

text
狮虎兽壹号 正在吃东西
狮虎兽壹号 使用老虎技能
狮虎兽壹号 使用狮子技能

虽然 Liger 类中只有一个 pass,但创建出来的对象可以使用三个层级中的内容:

  • eat() 和初始化逻辑最终来自 Animal
  • tiger_skill() 来自 Tiger
  • lion_skill() 来自 Lion

这就是多继承最直接的作用:让一个子类组合多个父类提供的能力。

MRO 与同名方法查找 ​

前面的两个父类都定义了 bark()。调用 liger.bark() 时,Python 必须决定使用 Lion.bark() 还是 Tiger.bark(),这正是 MRO 要解决的问题。

Python 会为每个类计算一条方法搜索顺序,简称 MRO,全称是 method resolution order。通过类的 __mro__ 属性可以查看这条顺序。

下面先打印类名,再调用 bark():

python
print([current_class.__name__ for current_class in Liger.__mro__])
liger.bark()


class ReversedLiger(Tiger, Lion):
    pass


reversed_liger = ReversedLiger('狮虎兽贰号', 1.4, 65)
print([current_class.__name__ for current_class in ReversedLiger.__mro__])
reversed_liger.bark()

运行结果:

text
['Liger', 'Lion', 'Tiger', 'Animal', 'object']
狮虎兽壹号 正在吼叫
['ReversedLiger', 'Tiger', 'Lion', 'Animal', 'object']
狮虎兽贰号 正在嗷呜叫

先看第一个类:

多继承 MRO 方法解析顺序:父类声明顺序决定同名方法先调用谁

text
Liger → Lion → Tiger → Animal → object

Python 先在 Liger 中查找 bark(),没有找到就继续查找 Lion。Lion 中已经存在这个方法,因此调用到这里就停止。

交换两个父类的顺序后,搜索顺序也发生变化:

text
ReversedLiger → Tiger → Lion → Animal → object

这一次先找到 Tiger.bark(),所以运行结果变成“正在嗷呜叫”。

以 __mro__ 为准

简单案例中,可以先从父类书写顺序判断查找方向。但继承关系复杂后,不要只凭感觉推断,直接查看类的 __mro__ 更可靠。

多继承初始化 ​

创建 Liger 时,Python 也会按照 MRO 查找 __init__()。当前案例中的 Lion 和 Tiger 都没有定义初始化方法,因此最终使用的是 Animal.__init__()。

如果多个父类分别保存不同的实例属性,初始化过程会复杂很多。此时各个类需要配合 super() 和统一的参数设计,才能沿着 MRO 依次完成初始化。

基础阶段先掌握下面两点:

  1. super() 在多继承中会沿着 MRO 继续查找,不一定只表示“调用代码里写出的某一个父类”。
  2. 多个父类都需要复杂初始化时,要谨慎设计,不要直接把多个类拼在一起。

多继承中 super() 的本质:沿 MRO 链的“下一棒”接力机制

多继承适用边界 ​

多继承适合表达“一个对象同时具有多类能力”的情况,但父类越多,属性、方法和初始化之间发生冲突的可能性就越大。决定使用前,需要先确认继承关系是否成立、方法查找是否清晰。

使用前可以先检查三个问题:

  1. 子类是否确实同时属于这些父类所描述的类型?
  2. 多个父类中是否存在大量同名属性或方法?
  3. 通过 __mro__ 能否清楚判断实际调用顺序?

如果多个类只是互相配合,并不存在明确的继承关系,优先让对象之间直接调用,不要为了少写几行代码强行使用多继承。

多继承适用边界:自查三问门禁与“组合优于继承”

小结 ​

本节重点掌握四件事:

  1. 多继承允许一个子类同时继承多个父类
  2. 子类可以使用不同父类提供的属性和方法
  3. 父类顺序会影响同名方法的查找结果
  4. __mro__ 可以查看真实的方法搜索顺序

多继承可以组合能力,但也会增加理解和维护成本。继承关系越复杂,越需要关注 MRO 和初始化过程。

练习 ​

  1. 定义 Phone 和 Camera,再让 SmartPhone 同时继承这两个类
  2. 分别调用两个父类提供的不同方法,确认子类可以同时使用
  3. 给两个父类增加同名的 start(),观察子类会调用哪一个版本
  4. 交换两个父类的顺序,再打印 SmartPhone.__mro__ 对比结果