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.eat;Dog.__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(对象, 父类或本类) |