面向对象高级(多继承)

本文深入剖析Python多继承中的方法解析顺序(MRO)机制,通过问界汽车案例演示父类顺序对__init__和同名方法调用的影响。重点揭示‘不报错却出错’的隐蔽陷阱:交换父类顺序后构造函数签名变化导致参数静默错配。强调多继承中super()仅沿MRO链向上一步,第二个父类必须显式调用,避免依赖默认行为引发的属性缺失或逻辑错误。

一辆车同时认两个爹:多继承里那个不报错的坑|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 无关——因为这里是“我自己点名调用”,不是“查找”。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值