| Day 16 | 继承与多态:MRO、super、方法重写与鸭子类型 |

1 继承的本质 —— 子类为什么「自动拥有」父类的方法

是什么?为什么学?

继承(inheritance)让一个类(子类)自动获得另一个类(父类)的属性和方法,不用重新写一遍。真实项目里,多个类有公共逻辑是常态:狗、猫、鸟都有名字、都要吃东西,把公共部分写进 Animal,三个子类各写各的特长——代码量骤减,改公共逻辑也只改一处。继承解决「复用」——公共代码只写一遍,子类自动拥有。

底层原理

类与类之间存在一条查找链,叫 MRO(方法解析顺序,Method Resolution Order)。访问 dog.eat() 时,Python 沿着 MRO 依次查找:先查 Dog 类,找不到就查 Animal,再找不到查 object(所有类的根)。子类里没定义 eat,但父类里有,所以能用——「自动拥有」的真相是查不到自己就去父类找,方法代码其实只有一份,并没有复制。

术语小词典:MRO(方法解析顺序)就是「类与类之间的查找链」——像家谱,查方法时按辈分一代代往上找。

生活类比

继承像祖传手艺:爷爷会做木工(父类),爸爸没学也「继承」了这份能力——不是重新发明,而是「要木工活时,按家谱(MRO)找到爷爷那份手艺来用」。

注意事项总结

1 子类没写 eat 也能调用;MRO 就是那条查找链

class Animal:
    def eat(self):
        print("正在吃东西")

class Dog(Animal):
    pass          # 子类一个字都不写

dog = Dog()
dog.eat()                          # 能用!
print("eat" in Dog.__dict__)       # Dog 自己没定义 eat
print("eat" in Animal.__dict__)    # 定义在 Animal 里
print(Dog.__mro__)                 # 查找链:Dog → Animal → object

运行结果:

正在吃东西
False
True
(<class '__main__.Dog'>, <class '__main__.Animal'>, <class 'object'>)

Dog(Animal) 括号里的 Animal 就是父类。dog.eat() 能执行,是因为沿着 MRO 找到了 Animal.eatDog.__mro__ 打印的就是查找链:先 Dog 自己,再父类 Animal,最后所有类的祖先 object子类并没有复制代码,只是「借用」。

易错点

  • 错误写法:继承写反:class Animal(Dog) → 问题:语义颠倒,Dog 还没定义时直接报 NameError → 正确写法:父类在前定义,子类写 class Dog(Animal)
  • 错误写法:以为子类会「复制」父类方法 → 问题:方法只有一份,子类只是按 MRO 借用 → 正确写法:理解 MRO:先查自己,再沿链向上找。
  • 错误写法:子类与父类方法同名却不想要父类行为 → 问题:同名即重写,父类版本被完全覆盖 → 正确写法:想保留部分父类行为,用 super() 调用父类版本。

记忆口诀:子类继承父类产,方法不复制只借用;查找沿 MRO,自己、父类、object。

2 super() 的真相 —— 先请父类「开个头」

是什么?为什么学?

super().__init__(...) 是在子类的方法里调用父类的同名方法,最常见的场景:子类 __init__ 先让父类初始化公共属性(如 name),再初始化自己特有的属性(如 breed)。不学 super,子类就得把父类的初始化代码重抄一遍,父类一改,所有子类跟着改。

底层原理

super() 返回一个特殊的代理对象,它沿着 MRO 从「当前类的下一个类」开始查找方法。比如 Dog 的 MRO 是 Dog → Animal → object,在 Dog 里写 super().__init__(name),就是「从 Animal 开始找 __init__」——于是父类的初始化逻辑被执行。它和「显式写 Animal.__init__(self, name)」效果类似,但用 super 更稳:父类改名、多继承时都不会写错对象。

生活类比

super()新员工入职培训:公司(父类)有一套公共培训(初始化 name),部门(子类)先安排你参加公司培训(super().__init__),再给你部门专属培训(设置 breed)——顺序不能反,先有公司身份,才有部门身份。

注意事项总结

1 不调 super 的后果 vs 调 super 的正确写法

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

class DogNoSuper(Animal):
    def __init__(self, name, breed):
        self.breed = breed     # 只初始化了自己的属性

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)    # 先让父类初始化 name
        self.breed = breed        # 再初始化自己的 breed

bad = DogNoSuper("旺财", "金毛")
print(bad.breed)
# print(bad.name)     # 取消注释会报 AttributeError:没有 name 属性
good = Dog("旺财", "金毛")
print(good.name, good.breed)

运行结果:

金毛
旺财 金毛

对比两个子类——DogNoSuper 重写了 __init__ 却没调 super,父类的 __init__ 不会自动执行name 根本没被初始化,一访问就炸;Dog 第一行写 super().__init__(name)不需要传 self),先初始化 name 再设置 breed——顺序讲究先父后子

易错点

  • 错误写法:子类重写 __init__ 却不调 super → 问题:父类属性没初始化,访问报 AttributeError → 正确写法:子类 __init__ 第一行写 super().__init__(...)
  • 错误写法:super 调用时手动传 self:super().__init__(self, name) → 问题:报参数个数错误(self 会被重复传入) → 正确写法:super().__init__(name),不要写 self。
  • 错误写法:忘了 super 的返回要用(x = super()) → 问题:不报错但 super 必须「当场点方法」用掉 → 正确写法:直接链式用:super().方法名(...)

记忆口诀:重写之后父类停,super 帮你开个头;先父后子讲顺序,调父方法不传 self。

3 方法重写 —— 子类按自己的方式做同一件事

是什么?为什么学?

重写(override)指子类定义与父类同名的方法,覆盖父类版本。动物都会「叫」,但狗汪汪、猫喵喵——行为按子类定制。重写让「同一个概念(speak)有统一的名字,具体做法各不相同」,这是多态(知识点 4)的基石。

底层原理

重写的原理就是 MRO 的「就近原则」:调用 dog.speak() 时沿 MRO 查找,Dog 自己定义了 speak第一个就命中,父类版本根本不会被看到。所以「重写」不是删除父类方法,而是在查找链上用自己的版本「挡住」父类版本。想要父类版本?super().speak() 绕过去。

生活类比

重写像同样的菜单,不同厨师:菜单上写着「招牌菜」(方法名 speak),川菜师傅做出来是辣的(汪汪),粤菜师傅做出来是清淡的(喵喵)——顾客只点「招牌菜」,具体谁做、怎么做,由后厨(运行时)决定。

注意事项总结

1 同名方法,各子类各版本

class Animal:
    def speak(self):
        return "动物叫了一声"

class Dog(Animal):
    def speak(self):
        return "汪汪!"

class Cat(Animal):
    def speak(self):
        return "喵喵~"

for obj in (Animal(), Dog(), Cat()):
    print(obj.speak())

运行结果:

动物叫了一声
汪汪!
喵喵~

三个类都有 speak,调用时按对象自己的类找——谁的对象,用谁的版本。父类的 speak 还在,只是被子类「挡」住了。

易错点

  • 错误写法:重写时方法名拼错(speek 而不是 speak) → 问题:不报错!但调用时走的是父类版本,行为「不对」 → 正确写法:重写必须与父类方法完全同名,写完检查。
  • 错误写法:重写后想保留父类行为却忘了 super → 问题:父类行为完全丢失,结果不完整 → 正确写法:需要父类行为时:super().方法名(...) 再补充。
  • 错误写法:重写方法参数与父类不一致 → 问题:可能报 TypeError(父类调用方按原参数调用) → 正确写法:重写时参数列表与父类保持一致。

记忆口诀:同名方法挡父版,就近命中不商量;要留父类一小块,super 调用再增强。

4 多态的本质 —— 动态分派,运行时才「点名」

是什么?为什么学?

多态(polymorphism)的意思是:同一个方法名,对不同对象调用,执行各自版本。写代码的人不需要知道对象具体是谁,只要它有这个方法就能调用。这让你能写「面对抽象」的通用代码——新加一个类,旧代码一行不改。继承解决「复用」,多态解决「灵活」。

底层原理

多态的底层机制叫动态分派(dynamic dispatch):obj.speak()运行时才根据 obj 的实际类型查找方法(沿 MRO 找 speak),而不是在写代码时定死。所以同一个函数里,传入的每个对象都会各自调用自己的版本——「对象自己决定怎么做」。

生活类比

多态像广播体操的「各就各位」:广播喊一声「集合!」(同一个调用),操场上的每个班(对象)按自己的位置站好(各自的行为)——广播不需要认识每个班的每个人,只要大家听得懂口令就行。

注意事项总结

1 不需要知道对象是谁,会「叫」就行;内置函数 len 也一样

class Dog:
    def speak(self):
        return "汪汪"

class Cat:
    def speak(self):
        return "喵喵"

class Duck:
    def speak(self):
        return "嘎嘎"

def chorus(animals):
    for a in animals:
        print(a.speak())

chorus([Dog(), Cat(), Duck()])

class Team:
    def __init__(self, members):
        self.members = members

    def __len__(self):
        return len(self.members)

print(len(Team(["张三", "李四", "王五"])))   # 自定义类的长度
print(len("abc"))                           # 字符串的长度
print(len([1, 2, 3]))                       # 列表的长度

运行结果:

汪汪
喵喵
嘎嘎
3
3
3

注意这三个类甚至没有共同的父类,但 chorus 照样工作——它只要求「对象有 speak 方法」。len(x) 的机制是「调用 x__len__ 方法」——同一函数、多种类型、各自实现,这就是多态在标准库里的体现。

2 同一个变量,不同阶段是不同对象——分派随之改变

class Animal:
    def speak(self):
        return "动物叫"

class Dog(Animal):
    def speak(self):
        return "汪汪"

animal = Dog()            # 此刻 animal 是 Dog 对象
print(animal.speak())     # 分派到 Dog 的版本
animal = Animal()         # 重新赋值成 Animal 对象
print(animal.speak())     # 分派到 Animal 的版本

运行结果:

汪汪
动物叫

两次 animal.speak() 写的是同一行代码,结果却不同,因为分派看的是当前对象实际是什么类。这就是「动态」二字:决定权在运行时、在对象身上。

易错点

  • 错误写法:用 if/elif 挨个判断类型再调方法(if type(x) == Dog: ...) → 问题:每加一个新类都要改这段判断 → 正确写法:让每个类自己实现同名方法,统一调用。
  • 错误写法:调用一个对象没有的方法(obj.fly() 但对象不会飞) → 问题:运行时才报 AttributeError → 正确写法:要么保证对象有该方法,要么先用 hasattr 检查。
  • 错误写法:以为多态必须继承同一个父类 → 问题:不需要!有同名方法即可(鸭子类型) → 正确写法:继承是「顺带」,约定方法名才是多态的关键。

记忆口诀:方法调用动态派,运行时看对象类;会叫就是鸭子,不必非是一家人。

5 isinstance 与鸭子类型 —— 两种「认人」方式

是什么?为什么学?

isinstance(对象, 类) 判断对象是不是某个类(或其子类)的实例——这是显式类型检查;鸭子类型则是不检查类型、只看行为:「会叫就让它叫」。两种思路各有适用场景:权限、专属功能(只有狗要洗澡)适合 isinstance;通用处理(所有会 speak 的都能表演)适合鸭子类型。

底层原理

isinstance(obj, Cls) 的判定依据是类型关系:检查 type(obj) 的 MRO 里有没有 Cls——有就是 True(所以子类实例对父类也返回 True)。而鸭子类型不做任何检查,直接调用方法,把「有没有这个方法」的风险推迟到运行时——代码更灵活,但错误也在运行时才暴露。多态优先用鸭子类型,特殊情况用 isinstance

生活类比

isinstance 像看证件:先查身份证确认是「员工」再放行进办公区(严格、安全);鸭子类型像看行为:只要会游泳、会嘎嘎叫,就按鸭子对待(宽松、灵活)。

注意事项总结

1 isinstance 的四种用法

class Animal: pass
class Dog(Animal): pass
class Cat(Animal): pass

dog = Dog()
print(isinstance(dog, Dog))            # 是 Dog 的实例
print(isinstance(dog, Animal))         # 也是 Animal 的实例(子类属于父类)
print(isinstance(dog, Cat))            # 不是 Cat 的实例
print(isinstance(dog, (Dog, Cat)))     # 元组:属于其中任一即可

运行结果:

True
True
False
True

第二行 True 是继承的体现——Dog 的 MRO 里有 Animal;第三行 False,Dog 和 Cat 是「兄弟」,互不包含;第四行 (Dog, Cat) 是元组,属于其中任意一个类(或其子类)即返回 True。

2 鸭子类型——不查证件,直接干活

class Dog:
    def speak(self):
        return "汪汪"

class ToyDog:
    def speak(self):
        return "汪汪(玩具电池音)"

for obj in (Dog(), ToyDog()):
    print(obj.speak())    # 不检查类型,直接调用

运行结果:

汪汪
汪汪(玩具电池音)

真狗和玩具狗没有继承关系,但都「会叫」,所以都能进来。约定方法名,比约定类名更灵活

易错点

  • 错误写法:type(obj) == Dog 判断类型 → 问题:对 Dog 的子类返回 False,把继承关系搞没了 → 正确写法:用 isinstance(obj, Dog)(含子类)。
  • 错误写法:到处用 isinstance 做分支 → 问题:每加一个新类都要改调用方代码,违背开闭原则 → 正确写法:优先让每个类实现同名方法(多态/鸭子类型)。
  • 错误写法:isinstance 参数写反:isinstance(Dog, obj) → 问题:不报错但永远返回 False → 正确写法:顺序是 isinstance(对象, 类)

记忆口诀:isinstance 看证件,MRO 里有就算;鸭子类型看行为,直接调用不设防;能多态就多态,专属功能再查证。

6 动手实践:动物类继承体系

把今天的内容串成一个完整的动物世界:

# animals.py —— 动物类继承体系
class Animal:
    """所有动物的基类"""
    def __init__(self, name):
        self.name = name

    def speak(self):
        return f"{self.name} 发出叫声"

    def move(self):
        return f"{self.name} 动了一下"

    def __str__(self):
        return f"{type(self).__name__}:{self.name}"


class Dog(Animal):
    def speak(self):
        return f"{self.name}:汪汪!"

    def move(self):
        return f"{self.name} 摇着尾巴跑起来了"


class Cat(Animal):
    def speak(self):
        return f"{self.name}:喵喵~"

    def move(self):
        return f"{self.name} 轻巧地跳上了沙发"


def animal_show(animals):
    """多态演示:同样的调用,不同的表现"""
    print("--- 动物园表演开始 ---")
    for animal in animals:
        print(animal)
        print(animal.speak())
        print(animal.move())


if __name__ == "__main__":
    dog = Dog("旺财")
    cat = Cat("咪咪")
    animal_show([dog, cat])

    # isinstance 演示
    print(isinstance(dog, Animal))   # True
    print(isinstance(cat, Dog))      # False

运行结果:

--- 动物园表演开始 ---
Dog:旺财
旺财:汪汪!
旺财 摇着尾巴跑起来了
Cat:咪咪
咪咪:喵喵~
咪咪 轻巧地跳上了沙发
True
False

注意 animal_show 里调用 animal.speak()animal.move(),狗和猫各自执行自己的版本——这就是多态。以后想加 Bird 类,只需继承 Animal 并重写方法,表演函数零改动。

升级挑战:新增 Bird 类(会飞,可以加 fly() 方法),加入表演;再给 Shape 体系加一个 Triangle 类,算三角形面积(底×高÷2)。

今日总结

继承的本质:子类不复制代码,调用时沿 MRO(自己 → 父类 → object)查找并借用父类方法 子类重写 __init__ 后父类的不会自动执行,要先 super().__init__(...),且调用时不要传 self 重写 = 用自己的同名方法沿 MRO「挡住」父类版本;要保留父类行为用 super().方法名(...) 多态 = 运行时动态分派:obj.方法() 按对象的实际类型选择版本,新类零改动即可接入 能写出「模板方法」式代码:父类定流程,子类重写细节 isinstance(obj, 类) 检查 MRO,子类实例对父类也返回 True;元组参数表示「任一即可」 能区分鸭子类型(看行为、直接调用)与 isinstance(看证件、按类型分流),知道优先多态 完成动物类继承体系,并尝试新增 Bird 类、Triangle 类

报错原因修复
TypeError: __init__() missing 1 required positional argument子类定义了 __init__ 却没调 super().__init__,父类参数没传在子类 __init__ 里先 super().__init__(...)
AttributeError: 'Dog' object has no attribute 'name'子类重写了 __init__ 但漏了父类属性初始化调用 super().__init__(name) 或自己初始化
TypeError: Class() takes no arguments误把类当函数调用并传参检查构造方法是否写成了普通函数(少 self
NameError(子类方法里用错变量名)拼写不一致,或想用父类属性却写成了局部变量核对 self.xxx 的拼写
isinstance 返回 False 但期望 True判断的类写反了,或用错类确认是 isinstance(对象, 父类或本类)
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇