继承与多态
这是什么:继承 = 子类在括号里认一个父类,自动获得父类全部属性和方法。
为什么用:狗会叫、猫会叫、仓鼠也要会叫,难道每个类都从零写一遍?不用继承,代码全是复制粘贴,改一处得改一片。
如何用:本章先从 class Dog(Animal) 写起,再讲同名方法重写和 super() 怎么接着父类的活干。
继承语法:class 子类(父类)
class Animal:
def __init__(self, name):
self.name = name
def eat(self):
print(f"{self.name}在吃饭")
class Dog(Animal): # 括号里写父类
def speak(self):
print(f"{self.name}:汪汪!")
d = Dog("旺财") # __init__ 也是继承来的
d.speak() # 自己的方法
d.eat() # 父类的方法,白拿
旺财:汪汪!
旺财在吃饭
| 写法 | 含义 |
|---|---|
class Dog(Animal) |
Dog 是子类,Animal 是父类;子类自动拥有父类全部属性和方法 |
d.speak()、d.eat() |
查找顺序:先找自己,再找父类,都找不到才报错 |
方法重写与 super()
父类的方法不合口味?子类定义同名方法即可重写(覆盖);想保留父类功能再加工,用 super() 调用父类版本。
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
print(f"{self.name}发出了声音")
class Cat(Animal):
def speak(self): # 重写:覆盖父类版本
print(f"{self.name}:喵~")
class Kitten(Animal):
def __init__(self, name):
super().__init__(name) # 借父类之手完成初始化
self.cute = True # 再添点自己的
Cat("咪咪").speak()
k = Kitten("奶球")
k.speak() # 没重写,原样用父类的
print(k.name, k.cute)
咪咪:喵~
奶球发出了声音
奶球 True
多态:同一句话,各家各腔
不同子类重写同一个方法后,同一行调用,不同对象有不同反应——这就是多态,对象版的"看人下菜碟"。
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
print(f"{self.name}发出了声音")
class Dog(Animal):
def speak(self):
print(f"{self.name}:汪汪!")
class Cat(Animal):
def speak(self):
print(f"{self.name}:喵~")
zoo = [Dog("旺财"), Cat("咪咪"), Dog("小黑")]
for a in zoo:
a.speak() # 同一句调用,猫猫狗狗各叫各的
旺财:汪汪!
咪咪:喵~
小黑:汪汪!
记忆口诀:括号里是爹,家底全继承;同名就翻新,super 找爹帮忙;一句 speak,各家唱各腔。
❓ 常见问题
Q1:子类重写了 __init__,却报 AttributeError: 'Kitten' object has no attribute 'name'?
原因:子类一旦定义了自己的 __init__,父类的就不再自动执行;新 __init__ 里没给 self.name 赋值,后面取 name 自然扑空。
修复三步:① 检查子类是不是自己写了 __init__;② 在里面补一句 super().__init__(name)(参数按父类的要求传);③ 再写子类新增的属性。
举一反三:重写 __init__ 的铁律——先 super 借爹之手干完老活,再干自己的新活。
Q2:写了 super.speak() 报 AttributeError: type object 'super' has no attribute 'speak'?
原因:super 后面忘了括号。super() 是"喊爹"这个动作,返回父类的代理人;光写 super 只是提到这个名字,Python 跑去一个叫 super 的类型身上找 speak,当然没有。
修复三步:① 定位报错行;② 给 super 补上括号:super().speak();③ 重跑,确认输出里多了父类的内容。
举一反三:见到 type object 'xxx' has no attribute,先怀疑"该加括号的地方没加"——super() 和实例化 Dog() 都是括号家族的。
Q3:子类没重写的方法,改了父类会影响子类吗?
原因:会,而且立刻生效。子类白拿的方法并没有抄一份存自己家里,而是"用的时候顺着血缘去父类拿"——父类一改,所有没重写的子类跟着变。 修复三步:① 改父类公共方法前,先数一数有几个子类在用;② 确认新行为对它们都成立再动手;③ 某个子类需要不同行为,就在它自己类里重写。 举一反三:这正是继承的威力也是风险——"改一处、全家更新",项目越大,父类越要谨慎修改。
Q4:什么时候该用继承,什么时候不该?
原因:判断口诀是"是不是"(is-a):狗是一种动物,通顺,继承;发动机是汽车的一部分(has-a),不通顺,别继承——把发动机做成汽车的属性装进去更合理。硬凑的继承会让代码越长越歪。
修复三步:① 造句"A 是一种 B";② 通顺就写 class A(B);③ 不通顺就改用"组合"——在 B 里放一个 A 类型的属性。
举一反三:ElectricCar(Car) 通顺;Engine(Car) 就很怪——发动机不是一种汽车,是汽车的一个零件。
Q5:循环里一句 a.speak(),Python 怎么知道该叫哪一声?
原因:这就是多态。列表里混装着 Dog、Cat 各家对象,调用时 Python 按每个对象自己的类找 speak:先找自己有没有重写,有就用自家的,没有才去父类——同一行代码,各家各腔。 修复三步:① 记住查找顺序"先自己、再父类";② 想让某家不同腔,就在它类里重写同名方法;③ 想整体换新玩法,只改父类,没重写的全家自动跟上。 举一反三:以后做"不同会员等级不同折扣""不同格式导出文件",都是"一个循环 + 各自重写"的多态套路,能省掉一大坨 if。
⚠️ 新手翻车现场
翻车一:重写 __init__ 却忘了喊爹
class Animal:
def __init__(self, name):
self.name = name
class Kitten(Animal):
def __init__(self, name): # 自己写了一套
self.cute = True # 忘了 super().__init__
k = Kitten("奶球")
print(k.name)
AttributeError: 'Kitten' object has no attribute 'name'
逐行分析:错在第 7~8 行——子类自定义 __init__ 后,父类那份不再自动执行,self.name 从没被赋值,第 8 行只挂了 cute。修复:在 self.cute = True 前加一句 super().__init__(name)。
翻车二:super 忘了括号
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
print("发出了声音")
class Cat(Animal):
def speak(self):
super.speak() # 忘了括号
Cat("咪咪").speak()
AttributeError: type object 'super' has no attribute 'speak'
逐行分析:错在第 11 行——super 后面没括号,Python 把它当成一个普通类型去找 speak 属性,当然找不到。补上括号变成 super().speak(),才是"喊爹执行"。
翻车三:重写时偷改参数清单
class Animal:
def __init__(self, name):
self.name = name
class Dog(Animal):
def speak(self, word): # 比父类多要一个参数
print(f"{self.name}:{word}")
d = Dog("旺财")
d.speak() # 一个参数都没给
TypeError: Dog.speak() missing 1 required positional argument: 'word'
逐行分析:病根在第 7 行——重写 speak 时偷偷加了个 word 参数;报错在第 12 行引爆——调用端还按老习惯不传参,供需对不上。重写方法时参数清单尽量和父类保持一致,要加就得所有调用处一起改。
📦 完整小示例
图形面积计算器:父类定骨架,子类各算各的公式,一个循环全部搞定。
class Shape:
def __init__(self, name):
self.name = name
def area(self):
print(f"{self.name}还不会算面积")
class Square(Shape):
def __init__(self, side):
super().__init__("正方形")
self.side = side
def area(self):
print(f"{self.name}面积:{self.side * self.side}")
class Circle(Shape):
def __init__(self, r):
super().__init__("圆形")
self.r = r
def area(self):
print(f"{self.name}面积:{3.14 * self.r * self.r}")
shapes = [Square(4), Circle(2), Square(10)]
for s in shapes:
s.area()
你会看到:
正方形面积:16
圆形面积:12.56
正方形面积:100
逐段中文精讲
- 第 1~6 行(父类兜底):Shape 收个名字,给了个"默认版" area——谁没重写就用它兜底,好比 Animal 里的"发出了声音"。
- 第 8~14 行(正方形):
super().__init__("正方形")借父类之手把名字挂好,再挂自己的 side;area 重写成"边长 × 边长"。 - 第 16~22 行(圆形):同一个两步走——super 初始化、重写 area 换成
3.14 × r × r。 - 第 24~25 行(多态上线):列表混装两种形状,一句
s.area()各算各的——Python 按对象自己的类找 area,正方形走正方形的公式,圆形走圆形的,循环体一行没变。
跑通后做个小实验:新建 Triangle(Shape),__init__ 收底和高,重写 area 打印"底 × 高 ÷ 2",把它塞进列表——循环一个字不改,新形状自动加入算面积大军。
🎯 什么时候用它
- 游戏单位:基础
Unit定好血量、移动,Warrior、Mage继承后只重写攻击方式;新职业上线就是新增一个子类的事。 - 支付方式:
Payment父类定好"pay"这个动作,微信支付、支付宝各自重写细节;收银台一句p.pay()通吃全家。 - 界面控件:
Widget定好"绘制、响应点击"的骨架,按钮、输入框继承后各自微调;界面上百个控件共享一套底座。
📝 本章小结
class 子类(父类)白拿父类全部家底;方法查找顺序:先自己、再父类- 子类定义同名方法即重写覆盖;
super().方法()借父类原版之力 - 重写
__init__记得先super().__init__(...),否则父类属性全体失踪 - 多态:循环里同一句调用,不同对象各干各的,省掉成堆 if
- 记忆锚点:继承是"拼爹开局",super 是"喊爹帮忙",重写是"青出于蓝",多态是"同一句口令、各家各动作"。
动手改
- 给
Animal加describe方法,打印"我是 + 名字",让 Dog、Cat 一行不写就拥有它 - 新建
Hamster(Animal),重写speak打印"吱吱!",把它加进 zoo 循环 - 把
Cat的speak改成:先打印"喵~",再调super().speak(),看输出变成什么样
下一章:魔术方法,解锁双下划线的隐藏能力 🐸