一辆车同时认两个爹:多继承里那个不报错的坑|Python 面向对象高级(3)
系列说明:第十一章「面向对象高级」学习笔记第 3 篇。前两篇写了封装和继承与方法重写,都是单继承。这一篇让子类同时继承两个父类,看看会发生什么。
例子我选了问界汽车
理由很直白:它本来就是“一辆车 + 一套华为智驾系统”拼出来的,拿来演示多继承再合适不过。
需求清单如下:
-
父类一
Car:brand/model/color/owner四个属性,以及start/run/stop/get_owner/charge五个方法。 -
父类二
HuaweiAIDriven:一个version属性(默认 V1.0),一个run方法。 -
子类
WenjieCar(Car, HuaweiAIDriven):两边都要。
关键冲突在于:两个父类都有 __init__,两个父类也都有 run()。子类如果不写任何东西,Python 到底听谁的?
完整代码
这个例子我分三个阶段跑:
# by tryer07 | Python 面向对象高级(3)多继承与 MRO · 学习笔记
class Car:
def __init__(self, brand, model, color, owner):
self.brand = brand
self.model = model
self.color = color
self.owner = owner
def start(self):
print(f"{self.brand} {self.model} 启动")
def run(self):
print(f"{self.owner}驾驶着{self.brand}{self.model}正在行驶")
def stop(self):
print(f"{self.brand} {self.model} 停止")
def get_owner(self):
print(f"拥有者是:{self.owner}")
def charge(self):
print(f"{self.brand} {self.model} 正在充电")
class HuaweiAIDriven:
def __init__(self, version="V1.0"):
self.version = version
def run(self):
print(f"使用华为AI智能驾驶系统 {self.version} 正在行驶")
# 阶段1:空子类,观察默认行为
class WenjieCar(Car, HuaweiAIDriven):
pass
print("---------- 阶段1:空子类的默认行为 ----------")
wc = WenjieCar("BMW", "X5", "黑色", "张三")
print("实例属性:", wc.__dict__)
print("MRO:", WenjieCar.__mro__)
wc.run()
wc.start()
wc.get_owner()
wc.charge()
wc.stop()
# 阶段2:交换父类顺序(用临时类演示,正式子类保持原顺序)
class WenjieCarSwapped(HuaweiAIDriven, Car):
pass
print("\n---------- 阶段2:交换父类顺序 ----------")
print("交换后 MRO:", WenjieCarSwapped.__mro__)
ok = WenjieCarSwapped("V2.0")
print("按新顺序传参成功:", ok.__dict__)
ok.run()
# 阶段3:子类重写 __init__ 与 run,把两个父类整合起来
class WenjieCar(Car, HuaweiAIDriven):
def __init__(self, brand, model, color, owner, version="V1.0"):
super().__init__(brand, model, color, owner)
HuaweiAIDriven.__init__(self, version)
def run(self):
Car.run(self)
HuaweiAIDriven.run(self)
print("\n---------- 阶段3:子类重写 __init__ 与 run ----------")
wc1 = WenjieCar("BMW", "X5", "黑色", "张三")
print("不传 version(默认值):", wc1.__dict__)
wc2 = WenjieCar("BMW", "X5", "黑色", "张三", "V1.1")
print("传 version(自定义值):", wc2.__dict__)
wc1.run()
wc2.run()
阶段1:一切看书写顺序
我先不写任何代码,直接跑一下空的 WenjieCar。结果发现:
-
__init__调的是排在第一个的Car的。证据不是靠猜,而是wc.__dict__里只有brand/model/color/owner四项,没有version。如果调的是HuaweiAIDriven.__init__,字典里应该只有version。 -
同理,
wc.run()输出的是「张三驾驶着BMWX5正在行驶」,而不是华为智驾的提示。同名的run(),也是第一个父类赢。
__mro__ 把这个规则摊开给你看了:WenjieCar -> Car -> HuaweiAIDriven -> object。
查找顺序就是这条链,从左到右,找到第一个就停。子类自己优先级最高,然后第一个父类,然后第二个父类,最后兜底到 object。
阶段2:一个不报错的坑(本节最值钱的部分)
按需求,我要“交换父类顺序,观察报错”。我把顺序换成 class WenjieCarSwapped(HuaweiAIDriven, Car),然后按原来的思路传一个参数试试:
WenjieCarSwapped("BMW")
我原本预期它报 TypeError——毕竟按 Car 的签名,"BMW" 只够填 brand,缺三个参数。
结果它一点错都没报。 "BMW" 被安安静静当成了 version。
交换顺序之后,__init__ 变成了 HuaweiAIDriven 的 def __init__(self, version="V1.0"),它有且只有一个可选参数。所以:
-
传 1 个位置参数 → 合法,"BMW" 落进
version,对象创建成功,属性字典里brand/model/color/owner全部消失。 -
传 4 个参数 → 才终于报错。
这件事比直接报错可怕多了。 报错你至少立刻知道改错了;而传 "BMW" 进去不报错,程序继续往下跑,等到某个地方读 self.brand 时才炸,或者更糟——不炸,只是数据一直是错的。
我提炼出一条习惯:多继承的子类,构造调用的参数个数一旦改变,绝不能靠“没报错”来判断正确,必须打印 obj.__dict__ 核对属性到底进没进来。 我在阶段1和阶段3都印了 __dict__,就是为了这个。
(顺便解释下为什么用临时类做对照组:同一个文件里没法同时保留两种顺序,改了跑一次再改回来,代码留不下证据,读者也看不到对比。用一个新类名做对照组,一次运行就能把两个顺序的输出并排打出来。)
阶段3:super() 只会往上走一步
要让问界汽车同时拥有 4 个车辆属性和 version,必须在子类重写 __init__:
def __init__(self, brand, model, color, owner, version="V1.0"):
super().__init__(brand, model, color, owner)
HuaweiAIDriven.__init__(self, version)
这里是本节最大的一个认知修正。
上一节我学到的 super() 是“调用父类”,很自然就会以为多继承里连续写两个 super().__init__() 就能把两个父类都初始化:
super().__init__(brand, model, color, owner)
super().__init__(version) # 天真写法,这是错的!
super() 不是“所有父类”,它是“MRO 里当前类的下一个”。
在这个类里,它永远指向 Car,第二行照样去找 Car.__init__,只是参数对不上而报错。
所以第二个父类必须显式点名:HuaweiAIDriven.__init__(self, version)。这也顺便说明了为什么上一节我要死记 父类.方法(self) 里那个 self——显式点名走的是普通函数调用路径,实例得你自己交出去。
于是两种风格的选择变得清楚了:
-
super():适合“我不关心是哪一个父类,沿链往上找”的单继承场景,父类改名也不用动代码。 -
父类名.方法(self):多继承里必须用它点名第二个、第三个父类,代价是父类改名要同步改代码。
run() 的重写同理,两个父类版本都想要,就只能分别显式调用:
def run(self):
Car.run(self)
HuaweiAIDriven.run(self)
输出确实是两行都打了,顺序就是代码里的书写顺序,跟 MRO 无关——因为这里是“我自己点名调用”,不是“查找”。
本文深入剖析Python多继承中的方法解析顺序(MRO)机制,通过问界汽车案例演示父类顺序对__init__和同名方法调用的影响。重点揭示‘不报错却出错’的隐蔽陷阱:交换父类顺序后构造函数签名变化导致参数静默错配。强调多继承中super()仅沿MRO链向上一步,第二个父类必须显式调用,避免依赖默认行为引发的属性缺失或逻辑错误。
&spm=1001.2101.3001.5002&articleId=168559432&d=1&t=3&u=2d16c7188ffc4632ad5a87cf20001d52)
1798

被折叠的 条评论
为什么被折叠?



