⌨️ 第五章 从设计到测试
📄 本篇摘要

继承=子类自动获得父类的属性与方法,再按需扩展(加新)或重写(改旧)。多父类(多重继承)能力更强但坑更多——Python 用 MRO 裁决冲突。经验法则:继承表达【是一种】;组合表达【有一个】;拿不准时组合优先。

📝 关系

上一节:类、实例与对象 | 下一节:字段、方法与属性

一、继承:复用的正道

        Animal(父类)
        属性 name / 方法 speak
       /            \
   Dog(子类)     Bird(子类)
   重写 speak      重写 speak + 新方法 fly

子类得到父类的全部家底,然后:

继承是【是一种】(is-a)关系:狗是一种动物,所以 Dog 继承 Animal 合理。

二、多重继承与 MRO

Python 允许一个类继承多个父类(C++ 也支持;Java 单继承+接口)。多继承的必答题:两个父类都有同名方法,听谁的?Python 的答案叫 MRO(方法解析顺序):按 C3 线性化算法排定查找顺序——先写在前的父类优先,且保证每个类只访问一次。查看:类名.__mro__。

教材同时警告:菱形继承(D 继承 B、C,B、C 都继承 A)是多继承经典陷阱——重复继承、构造顺序混乱,C++ 用虚继承、Python 用 MRO 化解。能不用多继承就不用:用组合或接口替代。

三、组合优于继承

【汽车有引擎】不该写成 Car 继承 Engine(汽车是一种引擎?错!)——Car 里放一个 Engine 成员(has-a)。组合的灵活性:换引擎不改车身;继承则是出生就焊死。这句【composition over inheritance】是面向对象最重要的经验法则之一。

四、动手 Demo:继承、重写、MRO 三连

class Animal:
    def __init__(self, name):
        self.name = name
    def speak(self):
        return "..."

class Bird(Animal):
    def speak(self):                      # 重写
        return "啾"
    def fly(self):                        # 扩展
        return f"{self.name} 会飞"

class Fish(Animal):
    def swim(self):
        return f"{self.name} 在游"

class FlyingFish(Bird, Fish):             # 多重继承:飞鱼 = 鸟 + 鱼
    pass

f = FlyingFish("尼莫")
print(f.speak())                          # MRO 先找到 Bird 的 speak
print(f.fly(), "/", f.swim())             # 两边的扩展都能用
print([c.__name__ for c in FlyingFish.__mro__])

# 重写后再调父类版本:super()
class LoudDog(Animal):
    def speak(self):
        return super().speak().upper() + "!汪!"
print(LoudDog("旺财").speak())

沙箱实跑输出:

啾
尼莫 会飞 / 尼莫 在游
['FlyingFish', 'Bird', 'Fish', 'Animal', 'object']
...!汪!

看到什么算成功:FlyingFish 会飞也会游;MRO 顺序打印清楚;LoudDog 在父类实现基础上再加工。

读结果:speak() 命中 Bird(MRO 里 Bird 在 Fish 前面);super() 不是【调父类】而是【沿 MRO 找下一个】——单继承时两者等价,多继承时 super 才显出真功力。重写不是推翻父类,常常是复用+增强。

五、30 秒实验

把 class FlyingFish(Bird, Fish) 的顺序改成 (Fish, Bird) 重跑——f.speak() 依然是【啾】(沙箱实测):Fish 自己的类字典里没有 speak,MRO 顺序 [飞鱼2, Fish, Bird, Animal] 接着查 Bird 就命中了。想让结果改变,得让 Fish 自己重写 speak——MRO 查的是整条链上先命中者,继承顺序即行为,多继承慎用。

六、常见误区

误区 真相
有代码可复用就继承 复用不是继承的理由;【是一种】才是;否则用组合
多继承多多益善 菱形结构、冲突裁决都是坑;能用组合或接口就不用
重写后父类实现就没了 super() 随时可回父类逻辑——重写常是【包装】而非【替换】
自测三问(点击展开) 1. **is-a 和 has-a 各对应什么?** 答:is-a(是一种)用继承;has-a(有一个)用组合——拿不准时组合优先。 2. **Python 多继承的同名冲突怎么裁决?** 答:MRO(C3 线性化):写在前的父类优先,`__mro__` 可查。 3. **菱形继承的麻烦是什么?** 答:同一祖先被访问两次、构造顺序混乱——C++ 虚继承/Python MRO 化解。

相关笔记