第7章 7.3 继承与多态

🎯 开场:为什么要学继承?

上一章我们学会了用 @property 给属性加保护罩,就像给房间门装上智能锁——外人只能看,不能直接进去乱翻。

但你有没有遇到过这种情况:你写了一个「学生」类,过两天又要写一个「研究生」类,发现研究生跟学生长得很像,但又不完全一样?

比如学生要管理成绩,研究生除了成绩还要管理研究方向。如果每个类都重新写一遍,代码就又臭又长了。

继承就是来解决这个问题的——让你站在巨人的肩膀上,新类复用老类的代码,只写不一样的地方。

这一章结束后,你就能:
- 用继承让代码少写一半
- 用多态让同一段代码操作不同对象
- 理解 Python 内置工具(如 strlist)为什么能通用


🧱 基础:继承与多态是什么?

7.3.1 继承:儿子继承老子的家产

类比时间:你家开了个煎饼摊,你爸传给你一套配方。你在这个配方基础上,加了自己的秘制酱料。这个过程就是继承——你"继承"了爸的配方,又做了自己的扩展。

在 Python 里:

# 父类(基类)- 老子
class Person:
def __init__(self, name, age):
    self.name = name
    self.age = age

def say_hi(self):
    print(f"你好,我叫{self.name},今年{self.age}岁")

# 子类(派生类)- 儿子
class Student(Person):  # 括号里写父类名,就是继承
def __init__(self, name, age, score):
    super().__init__(name, age)  # super()调用父类的__init__
    self.score = score  # 自己独有的属性

def say_hi(self):
    super().say_hi()
    print(f"我的成绩是{self.score}分")

# 测试
xiaoming = Student("小明", 18, 95)
xiaoming.say_hi()

输出:

你好,我叫小明,今年18岁
我的成绩是95分

解释:
- class Student(Person):Student 继承 Person,相当于"Student 是 Person 的子类"
- super().__init__(name, age):调用父类的初始化方法,避免重复写 self.name = name
- 子类可以有自己独有的属性(如 score),也可以重写父类的方法

配图1 - 配图1

7.3.2 方法重写:青出于蓝

子类可以重写(override)父类的方法,这样子类就用自己的逻辑,不用父类的。

class Animal:
def speak(self):
    print("动物发出声音")

class Dog(Animal):
def speak(self):  # 重写父类的speak方法
    print("汪汪汪")

class Cat(Animal):
def speak(self):  # 重写父类的speak方法
    print("喵喵喵")

dog = Dog()
cat = Cat()
dog.speak()  # 输出:汪汪汪
cat.speak()  # 输出:喵喵喵

输出:

汪汪汪
喵喵喵

解释: 子类可以完全替换父类的方法实现,这就是"重写"。同一个方法名,不同子类有不同行为。

7.3.3 多态:同一张菜单,不同厨师做

类比时间:你去饭店点"宫保鸡丁",川菜厨师做出来是辣的,粤菜厨师做出来是甜口的。同一个名字(宫保鸡丁),不同厨师做出来味道不一样——这就是多态。

在 Python 里:

def make_them_speak(animal):
"""让任何动物都说话"""
animal.speak()

class Pig(Animal):
def speak(self):
    print("哼哼哼")

pig = Pig()
make_them_speak(dog)   # 输出:汪汪汪
make_them_speak(cat)   # 输出:喵喵喵
make_them_speak(pig)   # 输出:哼哼哼

输出:

汪汪汪
喵喵喵
哼哼哼

解释:
- make_them_speak 函数不关心你传的是什么动物,只管调用 .speak()
- 只要对象有 speak 方法,就能用。类型不同,但行为类似——这就是多态

配图2 - 配图2

7.3.4 多继承:同时继承多个爹

Python 支持一个子类继承多个父类(多继承),但要注意顺序。

class Flyable:
def fly(self):
    print("我会飞!")

class Swimmable:
def swim(self):
    print("我会游泳!")

class Duck(Flyable, Swimmable):  # 同时继承两个类
def info(self):
    print("我是一只鸭子,又会飞又会游")

duck = Duck()
duck.fly()
duck.swim()
duck.info()

输出:

我会飞!
我会游泳!
我是一只鸭子,又会飞又会游

解释: class Duck(Flyable, Swimmable) 同时继承了两个类,duck 对象同时拥有两个父类的能力。

注意:多继承虽好,但不要滥用。如果两个父类有同名的方法,要看 MRO(方法解析顺序)。可以用 类名.__mro__ 查看。

7.3.5 抽象基类:定义规范,不做具体实现

类比时间:合同模板只规定"必须填姓名、联系电话",但不替你填——抽象基类就是这样,只定义规范,不做具体实现。

from abc import ABC, abstractmethod

class Shape(ABC):  # 抽象基类必须继承ABC
@abstractmethod
def area(self):  # 抽象方法:子类必须实现
    pass

@abstractmethod
def perimeter(self):
    pass

class Rectangle(Shape):
def __init__(self, width, height):
    self.width = width
    self.height = height

def area(self):
    return self.width * self.height

def perimeter(self):
    return 2 * (self.width + self.height)

rect = Rectangle(5, 3)
print(f"面积:{rect.area()}")       # 输出:面积:15
print(f"周长:{rect.perimeter()}")   # 输出:周长:16

解释:
- Shape 是抽象基类,规定"所有图形都必须有 areaperimeter 方法"
- @abstractmethod 装饰器标记抽象方法,子类必须重写,否则报错
- 如果不实现抽象方法,直接实例化会报 TypeError


🔥 实战:3 个递进小项目

项目 1:员工管理系统(5 分钟)

目标:用继承实现不同岗位的员工,有不同计算工资的方式。

class Employee:
def __init__(self, name, base_salary):
    self.name = name
    self.base_salary = base_salary

def get_salary(self):
    return self.base_salary

class Programmer(Employee):
def __init__(self, name, base_salary, overtime_hours):
    super().__init__(name, base_salary)
    self.overtime_hours = overtime_hours

def get_salary(self):
    return self.base_salary + self.overtime_hours * 200  # 加班费每小时200

class Sales(Employee):
def __init__(self, name, base_salary, commission):
    super().__init__(name, base_salary)
    self.commission = commission

def get_salary(self):
    return self.base_salary + self.commission  # 底薪+提成

# 测试
emp1 = Programmer("张三", 10000, 20)
emp2 = Sales("李四", 8000, 5000)

print(f"{emp1.name} 本月工资:{emp1.get_salary()} 元")
print(f"{emp2.name} 本月工资:{emp2.get_salary()} 元")

输出:

张三 本月工资:14000 元
李四 本月工资:13000 元

解释:两个子类都重写了 get_salary 方法,但调用方式一样——这就是多态的威力。


项目 2:从 JSON 读取数据构建继承结构(15 分钟)

目标:从 JSON 文件读取员工数据,动态创建不同类型的员工对象。

[
{"type": "programmer", "name": "王五", "base_salary": 15000, "overtime_hours": 30},
{"type": "sales", "name": "赵六", "base_salary": 8000, "commission": 8000},
{"type": "employee", "name": "钱七", "base_salary": 6000}
]
import json

class Employee:
def __init__(self, name, base_salary):
    self.name = name
    self.base_salary = base_salary

def get_salary(self):
    return self.base_salary

def __str__(self):
    return f"{self.__class__.__name__}({self.name}, 工资:{self.get_salary()})"

class Programmer(Employee):
def __init__(self, name, base_salary, overtime_hours):
    super().__init__(name, base_salary)
    self.overtime_hours = overtime_hours

def get_salary(self):
    return self.base_salary + self.overtime_hours * 200

class Sales(Employee):
def __init__(self, name, base_salary, commission):
    super().__init__(name, base_salary)
    self.commission = commission

def get_salary(self):
    return self.base_salary + self.commission

# 根据类型字符串创建对象
def create_employee(data):
emp_type = data.get("type", "employee")
if emp_type == "programmer":
    return Programmer(data["name"], data["base_salary"], data["overtime_hours"])
elif emp_type == "sales":
    return Sales(data["name"], data["base_salary"], data["commission"])
else:
    return Employee(data["name"], data["base_salary"])

# 读取JSON并创建对象
json_data = [
{"type": "programmer", "name": "王五", "base_salary": 15000, "overtime_hours": 30},
{"type": "sales", "name": "赵六", "base_salary": 8000, "commission": 8000},
{"type": "employee", "name": "钱七", "base_salary": 6000}
]

employees = [create_employee(data) for data in json_data]

print("=== 全体员工薪资单 ===")
for emp in employees:
print(emp)

输出:

=== 全体员工薪资单 ===
Programmer(王五, 工资:21000)
Sales(赵六, 工资:16000)
Employee(钱七, 工资:6000)

解释:通过字典映射类型字符串到类,然后用统一的接口(get_salary__str__)处理所有员工——这就是多态的实际应用。


项目 3:做一个简易日志记录器(15 分钟)

目标:做一个日志系统,支持多种输出方式(控制台、文件),通过继承轻松扩展新输出。

from datetime import datetime

class BaseLogger:
"""日志基类"""
def __init__(self, name):
    self.name = name

def log(self, level, message):
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    content = f"[{timestamp}] [{level}] {self.name}: {message}"
    self.write(content)

def write(self, content):
    """子类必须实现这个方法"""
    raise NotImplementedError

class ConsoleLogger(BaseLogger):
"""控制台日志"""
def write(self, content):
    print(content)

class FileLogger(BaseLogger):
"""文件日志"""
def __init__(self, name, filename):
    super().__init__(name)
    self.filename = filename

def write(self, content):
    with open(self.filename, "a", encoding="utf-8") as f:
        f.write(content + "\n")

class DualLogger(BaseLogger):
"""同时输出到控制台和文件"""
def __init__(self, name, filename):
    super().__init__(name)
    self.console = ConsoleLogger(name)
    self.file = FileLogger(name, filename)

def write(self, content):
    self.console.write(content)
    self.file.write(content)

# 使用
logger = DualLogger("系统", "app.log")
logger.log("INFO", "用户登录成功")
logger.log("WARNING", "磁盘空间不足")
logger.log("ERROR", "数据库连接失败")

print("\n--- 文件内容 ---")
with open("app.log", "r", encoding="utf-8") as f:
print(f.read())

输出:

[2024-01-15 10:30:00] [INFO] 系统: 用户登录成功
[2024-01-15 10:30:00] [WARNING] 系统: 磁盘空间不足
[2024-01-15 10:30:00] [ERROR] 系统: 数据库连接失败

--- 文件内容 ---
[2024-01-15 10:30:00] [INFO] 系统: 用户登录成功
[2024-01-15 10:30:00] [WARNING] 系统: 磁盘空间不足
[2024-01-15 10:30:00] [ERROR] 系统: 数据库连接失败

解释
- 基类 BaseLogger 定义了 log 的通用逻辑(时间戳、格式化)
- 子类只需实现 write 方法,就能自定义输出目标
- 想加新输出方式?新建一个子类就行,不改原有代码——这是"开闭原则"的经典示例


💪 进阶:常见坑 + 调试技巧

坑 1:忘记调用 super().init()

# ❌ 错误写法
class Child(Parent):
def __init__(self, name, extra):
    self.name = name  # 父类的name没初始化,可能出问题
    self.extra = extra

# ✅ 正确写法
class Child(Parent):
def __init__(self, name, extra):
    super().__init__(name)  # 先调父类的init
    self.extra = extra

坑 2:多继承时方法冲突

class A:
def greet(self):
    print("A说的你好")

class B:
def greet(self):
    print("B说的你好")

class C(A, B):
def greet(self):
    super(A, self).greet()  # 明确指定从哪个父类找

c = C()
c.greet()  # 输出什么?可以用 C.__mro__ 查看
print(C.__mro__)  # (<class 'C'>, <class 'A'>, <class 'B'>, <class 'object'>)

坑 3:抽象类不能直接实例化

from abc import ABC, abstractmethod

class Base(ABC):
@abstractmethod
def run(self):
    pass

# ❌ 错误
# base = Base()  # TypeError: Can't instantiate abstract class

# ✅ 正确:先继承,再实例化子类
class Impl(Base):
def run(self):
    print("运行中")

impl = Impl()  # OK

坑 4:重写方法时参数不一致

class Parent:
def process(self, x, y):
    return x + y

class Child(Parent):
def process(self, x):  # ❌ 参数少了
    return x * 2

# child = Child()
# child.process(1, 2)  # TypeError

坑 5:用可变对象做默认参数

class Container:
def __init__(self, items=None):  # ❌ 常见错误
    if items is None:
        items = []
    self.items = items

调试技巧

# 查看类的继承链
print(类名.__mro__)

# 查看对象有哪些方法
print(dir(对象))

# 用type()检查对象类型
print(type(obj).__name__)

✏️ 练习题

练习 1(2 分钟):添加新员工类型
- 输入:在项目 1 中添加一个 Manager 类,基本工资 15000,奖金 10000
- 预期输出:Manager("周八", 15000, 10000).get_salary() 返回 25000
- 提示:super().__init__() 传基本工资,自定义逻辑加奖金

练习 2(2 分钟):重写 say_hi
- 输入:在项目 1 的 Programmer 类里重写 say_hi 方法,输出包含"我是一名程序员"
- 预期输出:调用时输出包含"程序员"和"工资"
- 提示:先用 super().say_hi() 再加自己的 print

练习 3(5 分钟):处理新 JSON 数据
- 输入:用项目 2 的方法,添加一个包含 5 个员工的 JSON 列表
- 预期输出:正确输出每个员工的类型和工资
- 提示:只需修改 json_data 变量,其他代码不用动

练习 4(8 分钟):组合日志和员工系统
- 输入:把项目 3 的日志功能加到项目 2,发工资时自动记录日志
- 预期输出:运行后同时看到控制台输出和日志文件内容
- 提示:在 create_employee 里给对象挂一个 logger 属性

练习 5(5 分钟):分析报错
- 输入:运行以下代码会报错,分析原因

class A:
def test(self):
    print("A")

class B(A):
def test(self):
    print("B")

class C(A):
def test(self):
    print("C")

class D(B, C):
pass

D().test()  # 输出什么?为什么?
  • 预期输出:解释 MRO 的工作原理
  • 提示:用 D.__mro__ 查看顺序

作业:做一个宠物店管理系统

需求:管理不同类型的宠物,有不同计算价格的方式。

功能点
1. 宠物基类,包含名字、价格、计算总价方法
2. 猫、狗、鸟三个子类,继承基类
3. 从 JSON 读取宠物数据,动态创建对象
4. 支持多态:同一个函数处理所有宠物

加分项
1. 添加新宠物类型(兔子、鱼)只需加代码不改动原有代码
2. 用抽象基类规范所有宠物必须有"计算总价"方法

验收标准
- 能运行,输出所有宠物的名称和价格
- 代码有注释
- 提交方式:评论区贴代码


📚 总结

这一章学了 3 件事:
1. 继承:子类复用父类代码,用 super() 调用父类方法
2. 方法重写:子类替换父类方法实现
3. 多态:同一接口,不同对象有不同行为

延伸资源
- 官方文档:https://docs.python.org/3/tutorial/classes.html#inheritance
- 《Python编程:从入门到实践》第 9 章
- 视频:B 站小甲鱼 Python 教程第 27-28 讲

互动钩子:你在实际项目中用过继承吗?是怎么组织的?评论区聊聊老粉优先回复!

预告:下一章我们要学习类的"特殊方法"——__str____len____add__ 这些带双下划线的方法,它们能让你的对象支持 len()+print() 等内置操作。敬请期待!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。