跳到主要内容

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__eatsleep 完全相同。哪天想给吃饭加个"吃完打个饱嗝",得改三处——和 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 实战项目一:猜数字游戏