多继承基础
学习层次
本章属于拓展回查。首次学习先掌握多继承语法、同名方法冲突和 __mro__ 的查看方式;复杂的协作式初始化不作为基础阶段的独立编写要求。
上一节学习的单继承中,一个子类只直接继承一个父类。但有些对象需要同时获得多个类提供的能力。一个对象什么时候适合拥有多个父类?父类顺序变化后,方法调用结果会不会变化?
例如,狮虎兽既需要老虎的技能,也需要狮子的技能。如果只继承其中一个类,另一部分能力就无法直接使用。这时可以让一个子类同时继承多个父类,这就是多继承。本节先掌握语法、同名方法查找和初始化边界,再判断多继承是否适合当前关系。
多继承适用场景
继续使用动物场景。现在要定义一个狮虎兽类,它需要同时获得老虎和狮子的能力:
狮虎兽(Liger)
公共属性:name、high、weight
公共方法:eat
老虎提供:bark、tiger_skill
狮子提供:bark、lion_skill如果完全不使用继承,Liger 就要再次编写公共属性、eat() 和两类动物的技能:
这些代码虽然可以运行,但是重复内容较多。使用多继承后,Liger 可以同时获得 Tiger 和 Lion 提供的能力:
多继承语法
定义子类时,在括号中依次写出多个父类:
class 子类名(父类名1, 父类名2):
pass例如:
class Liger(Lion, Tiger):
passLiger 同时继承 Lion 和 Tiger。括号中父类的先后顺序不是随便写的,它会影响同名方法的查找结果。
先让案例运行起来,再观察父类顺序的具体影响。
案例:狮虎兽多继承实现
需求:定义 Liger,让它同时获得 Lion 和 Tiger 的能力,并观察多个父类提供的方法如何协作。
功能分析:先定义两个共享 Animal 的父类,再创建 Liger 实例,最后通过同名方法调用观察父类顺序的影响。
父类定义
Animal 保存三种动物共有的属性和 eat()。Tiger、Lion 分别继承 Animal,再增加各自的叫声和技能:
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:
class Liger(Lion, Tiger):
pass
liger = Liger('狮虎兽壹号', 1.3, 60)
liger.eat()
liger.tiger_skill()
liger.lion_skill()运行结果:
狮虎兽壹号 正在吃东西
狮虎兽壹号 使用老虎技能
狮虎兽壹号 使用狮子技能虽然 Liger 类中只有一个 pass,但创建出来的对象可以使用三个层级中的内容:
eat()和初始化逻辑最终来自Animaltiger_skill()来自Tigerlion_skill()来自Lion
这就是多继承最直接的作用:让一个子类组合多个父类提供的能力。
MRO 与同名方法查找
前面的两个父类都定义了 bark()。调用 liger.bark() 时,Python 必须决定使用 Lion.bark() 还是 Tiger.bark(),这正是 MRO 要解决的问题。
Python 会为每个类计算一条方法搜索顺序,简称 MRO,全称是 method resolution order。通过类的 __mro__ 属性可以查看这条顺序。
下面先打印类名,再调用 bark():
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()运行结果:
['Liger', 'Lion', 'Tiger', 'Animal', 'object']
狮虎兽壹号 正在吼叫
['ReversedLiger', 'Tiger', 'Lion', 'Animal', 'object']
狮虎兽贰号 正在嗷呜叫先看第一个类:
Liger → Lion → Tiger → Animal → objectPython 先在 Liger 中查找 bark(),没有找到就继续查找 Lion。Lion 中已经存在这个方法,因此调用到这里就停止。
交换两个父类的顺序后,搜索顺序也发生变化:
ReversedLiger → Tiger → Lion → Animal → object这一次先找到 Tiger.bark(),所以运行结果变成“正在嗷呜叫”。
以 __mro__ 为准
简单案例中,可以先从父类书写顺序判断查找方向。但继承关系复杂后,不要只凭感觉推断,直接查看类的 __mro__ 更可靠。
多继承初始化
创建 Liger 时,Python 也会按照 MRO 查找 __init__()。当前案例中的 Lion 和 Tiger 都没有定义初始化方法,因此最终使用的是 Animal.__init__()。
如果多个父类分别保存不同的实例属性,初始化过程会复杂很多。此时各个类需要配合 super() 和统一的参数设计,才能沿着 MRO 依次完成初始化。
基础阶段先掌握下面两点:
super()在多继承中会沿着 MRO 继续查找,不一定只表示“调用代码里写出的某一个父类”。- 多个父类都需要复杂初始化时,要谨慎设计,不要直接把多个类拼在一起。
多继承适用边界
多继承适合表达“一个对象同时具有多类能力”的情况,但父类越多,属性、方法和初始化之间发生冲突的可能性就越大。决定使用前,需要先确认继承关系是否成立、方法查找是否清晰。
使用前可以先检查三个问题:
- 子类是否确实同时属于这些父类所描述的类型?
- 多个父类中是否存在大量同名属性或方法?
- 通过
__mro__能否清楚判断实际调用顺序?
如果多个类只是互相配合,并不存在明确的继承关系,优先让对象之间直接调用,不要为了少写几行代码强行使用多继承。
小结
本节重点掌握四件事:
- 多继承允许一个子类同时继承多个父类
- 子类可以使用不同父类提供的属性和方法
- 父类顺序会影响同名方法的查找结果
__mro__可以查看真实的方法搜索顺序
多继承可以组合能力,但也会增加理解和维护成本。继承关系越复杂,越需要关注 MRO 和初始化过程。
练习
- 定义
Phone和Camera,再让SmartPhone同时继承这两个类 - 分别调用两个父类提供的不同方法,确认子类可以同时使用
- 给两个父类增加同名的
start(),观察子类会调用哪一个版本 - 交换两个父类的顺序,再打印
SmartPhone.__mro__对比结果