9.2 继承
上一节你已经会造类了。这一节解决"造很多个相似的类"时的重复问题——继承(inheritance),面向对象的第二根支柱。
先感受痛点:三个类,两份半重复代码
游戏里要做猫、狗、鸭三种宠物,都有名字、会吃饭、会睡觉,只是叫声各不相同。按上一节的写法:
class Cat:
def __init__(self, name):
self.name = name
def eat(self):
print(f"{self.name} 在吃饭")
def sleep(self):
print(f"{self.name} 在睡觉")
def speak(self):
print(f"{self.name}:喵喵~")
class Dog:
def __init__(self, name):
self.name = name
def eat(self):
print(f"{self.name} 在吃饭") # 和 Cat 一模一样
def sleep(self):
print(f"{self.name} 在睡觉") # 和 Cat 一模一样
def speak(self):
print(f"{self.name}:汪汪!") # 只有这里不同
Duck 还得再抄一遍……三个类里 __init__、eat、sleep 完全相同。哪天想给吃饭加个"吃完打个饱嗝",得改三处——和 2.1 里"名字重复三处"是同一种病,只是这次重复的是整个类的骨架。
解药也是同一个思路:把共同部分提取出来,写一次。提取出来的类叫父类,各自继承它的叫子类:
class Animal: # 父类:装所有宠物的共同点
def __init__(self, name):
self.name = name
def eat(self):
print(f"{self.name} 在吃饭")
def sleep(self):
print(f"{self.name} 在睡觉")
class Cat(Animal): # 括号里写父类名 → Cat 继承 Animal
def speak(self):
print(f"{self.name}:喵喵~")
class Dog(Animal):
def speak(self):
print(f"{self.name}:汪汪!")
验证一下——子类里一行没写 eat,却直接能用:
cat = Cat("咪咪")
cat.eat() # 咪咪 在吃饭 ← 从 Animal 白捡的
cat.speak() # 咪咪:喵喵~ ← 自己新增的
dog = Dog("旺财")
dog.sleep() # 旺财 在睡觉
dog.speak() # 旺财:汪汪!
语法只有一处新东西:
class Cat(Animal):
↑ ↑
子类 父类(写在括号里)
子类自动拥有父类的全部属性和方法,在此基础上还能加自己的新方法。以后想给吃饭加饱嗝,只改 Animal 一处,猫狗鸭全部生效。
💡 术语对照:父类也叫基类、超类;子类也叫派生类。“Cat 继承 Animal”,本质是在declaring一层"是一种"的关系:猫是一种动物。判断该不该用继承,就问这句话通不通顺——"程序员是一种员工"✅ 适合继承;"汽车是一种轮子"❌ 不通,轮子应该做汽车的属性。
方法是怎么找到的:先自己,再父类
cat.eat() 时 Python 的查找顺序:先看 Cat 自己有没有 eat → 没有 → 去父类 Animal 找 → 找到,执行。都找不到才报 AttributeError。
这条查找顺序直接推出下一个特性——
方法重写:子类不满意,就自己写一个
如果子类定义了和父类同名的方法,按"先自己再父类"的顺序,自家版本先被找到,父类版本就被覆盖了。这叫重写(override):
class Animal:
def speak(self):
print("动物在叫(具体怎么叫我也不知道)")
class Cat(Animal):
def speak(self): # 重写:同名方法覆盖父类版本
print("喵喵~")
class Duck(Animal):
pass # 什么都不写,全盘继承
Cat().speak() # 喵喵~ ← 用的是自家版本
Duck().speak() # 动物在叫…… ← 自己没有,用父类版本
规则就一句:同名即覆盖,没有就上溯。
super():想补充,而不是完全替换
重写是"推翻重来",但更多时候子类想在父类版本的基础上加点东西。最典型的场景:子类比父类多一个属性。
class Animal:
def __init__(self, name):
self.name = name
class Dog(Animal):
def __init__(self, name, breed): # 狗比普通动物多一个"品种"
super().__init__(name) # ① 先请父类把 name 装好
self.breed = breed # ② 再装自己特有的 breed
dog = Dog("旺财", "柴犬")
print(dog.name) # 旺财
print(dog.breed) # 柴犬
super() 的意思是"我的父类",super().__init__(name) 就是"调用父类的 __init__,让它干它会干的活"。注意调用时不传 self,和平时调用方法一样由 Python 自动处理。
⚠️ 本节头号大坑:重写了
__init__却忘调super().__init__()。class Dog(Animal):def __init__(self, name, breed):self.breed = breed # ❌ 忘了 super().__init__(name)dog = Dog("旺财", "柴犬")print(dog.breed) # 柴犬 —— 这里正常,让你误以为没问题print(dog.name) # AttributeError: 'Dog' object has no attribute 'name'险恶之处在于创建对象时不报错,用到缺失属性那一刻才炸。原理:子类一旦定义了自己的
__init__,父类的不再自动执行——装 name 的那行代码根本没跑过。记住铁律:子类重写__init__时,第一行先写super().__init__(...)。反过来,像前面 Cat 那样完全不写
__init__,父类的会自动被继承使用,不用调 super。
super() 不只用于 __init__,重写普通方法时同样能"先执行父类版本,再加料":
class Cat(Animal):
def eat(self):
super().eat() # 先照常吃饭
print(f"{self.name} 舔了舔爪子") # 再补充猫特有的动作
Cat("咪咪").eat()
# 咪咪 在吃饭
# 咪咪 舔了舔爪子
综合实战:员工系统与多态
把继承、重写、super 串进一个更真实的例子:
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def work(self):
print(f"{self.name} 在工作")
def get_pay(self):
return self.salary
class Programmer(Employee):
def work(self): # 重写:程序员的工作方式
print(f"{self.name} 在写代码")
class Manager(Employee):
def __init__(self, name, salary, bonus):
super().__init__(name, salary) # 老规矩:先让父类初始化
self.bonus = bonus
def get_pay(self): # 重写:经理多拿奖金
return self.salary + self.bonus
staff = [
Programmer("小明", 15000),
Manager("老王", 20000, 5000),
Employee("小红", 8000),
]
for s in staff:
s.work()
print(f" {s.name} 月收入:{s.get_pay()} 元")
输出:
小明 在写代码
小明 月收入:15000 元
老王 在工作
老王 月收入:25000 元
小红 在工作
小红 月收入:8000 元
重点看那个循环:它根本不关心 s 是程序员还是经理,统一喊 s.work()、s.get_pay(),每个对象自动表现出自己的版本。这个特性叫多态——同一句调用,不同对象各有各的响应。它的价值在于:以后新增 Designer 类,这个循环一个字都不用改。名字先记住,用多了自然懂。
isinstance():这对象是哪类的
偶尔需要判断对象的类型,用 isinstance(对象, 类):
m = Manager("老王", 20000, 5000)
print(isinstance(m, Manager)) # True
print(isinstance(m, Employee)) # True —— 经理"是一种"员工,子类对象也算父类的实例
print(isinstance(m, Programmer)) # False
注意第二行:继承关系里,子类对象天然被认作父类的实例——这正是"经理是一种员工"在代码层面的体现。顺带一提,Python 里所有类都默认继承自一个终极父类 object,所以任何对象 isinstance(x, object) 都是 True。
本节报错速查表
| 报错/异常现象 | 人话翻译 |
|---|---|
AttributeError: 'Dog' object has no attribute 'name'(创建时没报错,用时才报) | 子类重写 __init__ 后忘了调 super().__init__(...) |
TypeError: __init__() missing 1 required positional argument | 创建子类对象时,参数没按子类的 __init__ 传(子类重写后以子类为准) |
| 调用方法执行的不是预期版本 | 检查子类是否重写了同名方法——同名即覆盖 |
练习
新建 practice92.py 完成:
1. 动手:定义 Shape 父类(area() 方法返回 0);Circle(属性半径)和 Rectangle(属性长、宽)继承并重写 area()。把两个图形放进列表,循环打印各自面积——体验多态。
2. 猜输出:先别运行,猜猜打印什么?
class A:
def hello(self):
print("A 的 hello")
def hi(self):
print("A 的 hi")
class B(A):
def hello(self):
print("B 的 hello")
b = B()
b.hello()
b.hi()
3. 修 bug:下面的代码在最后一行报 AttributeError,找出原因修好它:
class User:
def __init__(self, username):
self.username = username
class VIPUser(User):
def __init__(self, username, level):
self.level = level
v = VIPUser("xiaoming", 3)
print(v.level)
print(v.username)
4. 挑战:基于 9.1 练习的 BankAccount(有 owner、balance、deposit、withdraw),派生一个 SavingsAccount(储蓄账户):新增属性 rate(利率),新增方法 add_interest() 把 balance * rate 的利息存进账户(内部直接复用父类的 deposit 方法)。
点击查看答案
# 1
import math
class Shape:
def area(self):
return 0
class Circle(Shape):
def __init__(self, r):
self.r = r
def area(self):
return math.pi * self.r ** 2
class Rectangle(Shape):
def __init__(self, w, h):
self.w = w
self.h = h
def area(self):
return self.w * self.h
shapes = [Circle(2), Rectangle(3, 4)]
for s in shapes:
print(f"{s.area():.2f}") # 12.57 和 12.00
2. 输出:
B 的 hello
A 的 hi
hello 被 B 重写,用自家版本;hi 自己没有,上溯到父类 A。——"同名即覆盖,没有就上溯"。
3. VIPUser 重写了 __init__ 却没调父类的,username 从未被装到对象上:
class VIPUser(User):
def __init__(self, username, level):
super().__init__(username) # 补上这行,先让父类装好 username
self.level = level
# 4
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("存款金额必须大于 0")
self.balance += amount
print(f"存入 {amount} 元,余额 {self.balance} 元")
def withdraw(self, amount):
if amount > self.balance:
print("余额不足!")
return False
self.balance -= amount
return True
class SavingsAccount(BankAccount):
def __init__(self, owner, balance=0, rate=0.03):
super().__init__(owner, balance)
self.rate = rate
def add_interest(self):
interest = self.balance * self.rate
self.deposit(interest) # 直接复用父类的存钱逻辑(含金额校验)
s = SavingsAccount("小明", 1000, 0.05)
s.add_interest() # 存入 50.0 元,余额 1050.0 元
第 4 题的妙处:add_interest 里没有重复写"余额 +=",而是调用继承来的 deposit——复用而不是复制,正是继承的意义。
本节小结
class 子类(父类):—— 子类自动获得父类的全部属性和方法;判断标准是"子类是一种父类"- 方法查找顺序:先自己 → 再父类;同名即覆盖(重写),没有就上溯
- 子类想"补充"而非"替换"时用
super();重写__init__必须第一行调super().__init__(...),忘了就是延迟引爆的 AttributeError - 多态:同一句调用,不同对象各自响应——新增子类不用改旧代码
isinstance(对象, 类)判断类型,子类对象也算父类的实例
基础知识全部学完!🎉 接下来用三个实战项目检验成果:10.1 实战项目一:猜数字游戏