Python面向对象程序设计练习题:类与继承实战

Ivy
Ivy
发布时间:2026-10-25 | 4人浏览过
原创

Python面向对象程序设计(OOP)的核心在于将现实问题抽象为封装的数据与行为,并通过继承与多态实现逻辑复用与扩展。面向对象的语法虽然简洁,但在实际工程开发中,开发者常常面临“如何合理划分属性封装边界”、“何时使用继承而非组合”、“super() 在复杂继承下的调用链条”等具体挑战。本文精选四个层层递进的经典实战练习题,分别涵盖基础类封装、单继承与方法重写、多继承与 MRO 解析、抽象基类与接口契约设计。每个题目均包含完整的场景需求、工程化实现代码、执行结果说明及易错点剖析,帮助读者建立严谨的面向对象设计思维。

练习题一:电商库存管理——类的封装与属性控制

在电商系统中,直接暴露对象属性容易引发脏数据写入风险。例如负库存、非法价格变动等操作可能破坏业务数据一致性。合理的做法是将关键数据私有化或保护化,仅通过定义明确的属性装饰器与方法对外提供受控访问。

题目需求

  1. 设计 Product 商品类,初始化时接收商品名称 name、单价 price 与初始库存 stock。
  2. name 一经初始化不可变更;price 必须为大于 0 的正数;stock 必须为大于等于 0 的整数。
  3. 提供 restock(quantity) 补货方法与 deduct(quantity) 出库扣减方法,出库数量大于现有库存时抛出异常并阻止扣减。
Python面向对象程序设计练习题:类与继承实战
图 1:Product 类封装结构与出入库校验防护流程

参考代码实现

# Python 3.8+ 适用环境
class Product:
    def __init__(self, name: str, price: float, stock: int):
        self._name = str(name).strip()
        if not self._name:
            raise ValueError("商品名称不能为空")
        self._price = None
        self._stock = None
        # 通过 setter 统一完成初值校验
        self.price = price
        self.stock = stock

    @property
    def name(self) -> str:
        """只读属性:商品名称不可直接赋值篡改"""
        return self._name

    @property
    def price(self) -> float:
        return self._price

    @price.setter
    def price(self, value: float):
        if not isinstance(value, (int, float)) or value <= 0:
            raise ValueError(f"单价必须为大于0的数值,当前输入: {value}")
        self._price = float(value)

    @property
    def stock(self) -> int:
        return self._stock

    @stock.setter
    def stock(self, value: int):
        if not isinstance(value, int) or value < 0:
            raise ValueError(f"库存必须为非负整数,当前输入: {value}")
        self._stock = value

    def restock(self, quantity: int):
        if not isinstance(quantity, int) or quantity <= 0:
            raise ValueError("补货数量必须为正整数")
        self._stock += quantity

    def deduct(self, quantity: int) -> bool:
        if not isinstance(quantity, int) or quantity <= 0:
            raise ValueError("扣减数量必须为正整数")
        if quantity > self._stock:
            raise ValueError(f"库存不足,当前库存: {self._stock},请求扣减: {quantity}")
        self._stock -= quantity
        return True

    def __repr__(self) -> str:
        return f"Product(name='{self._name}', price={self._price:.2f}, stock={self._stock})"

测试执行与结果输出

# 执行测试代码
item = Product("机械键盘", 399.0, 10)
print("初始商品:", item)

item.deduct(3)
print("出库 3 件后库存:", item.stock)

try:
    item.deduct(15)
except ValueError as err:
    print("出库拦截提示:", err)

try:
    item.price = -50
except ValueError as err:
    print("改价拦截提示:", err)
# 示例输出:
初始商品: Product(name='机械键盘', price=399.00, stock=10)
出库 3 件后库存: 7
出库拦截提示: 库存不足,当前库存: 7,请求扣减: 15
改价拦截提示: 单价必须为大于0的数值,当前输入: -50
避坑要点:单下划线与双下划线的选择

Python 社区标准约定使用单下划线(如 _price)表示受保护的内部属性,配合 @property 即可满足绝大多数业务校验。避免滥用双下划线(如 __price),双下划线会触发名称重整(Name Mangling),主要是为了避免多重继承命名冲突,并不提供真正的安全私有化。

练习题二:员工薪酬结算系统——单继承、方法重写与 super()

单继承是面向对象代码复用的基石。派生类在继承基类公共特征的同时,可以扩展私有属性,并通过重写(Override)基类方法实现各自独立的业务逻辑。在这个过程中,正确使用 super() 是保证初始化链路完整执行的核心。

题目需求

  1. 设计基类 Employee,统一维护工号 emp_id 和姓名 name,并提供默认的月度薪资计算方法 calculate_pay()。
  2. 创建派生类 SalariedEmployee(月薪制员工),增加基础月薪属性 monthly_salary。
  3. 创建派生类 HourlyEmployee(时薪制员工),增加时薪 hourly_rate 与当月工时 hours_worked,超过 160 小时的部分按 1.5 倍加班费核算。
  4. 两个子类均需重写 calculate_pay(),并在上层以列表形式统一批量遍历结算。
Python面向对象程序设计练习题:类与继承实战
图 2:单继承与 super() 协同初始化执行时序与数据流转

参考代码实现

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

    def calculate_pay(self) -> float:
        """基类默认方法,提供缺省契约"""
        return 0.0

    def get_details(self) -> str:
        return f"[{self.emp_id}] {self.name} - 应发薪资: ¥{self.calculate_pay():.2f}"


class SalariedEmployee(Employee):
    def __init__(self, emp_id: str, name: str, monthly_salary: float):
        # 通过 super() 执行基类初始化
        super().__init__(emp_id, name)
        if monthly_salary < 0:
            raise ValueError("月薪不能为负数")
        self.monthly_salary = float(monthly_salary)

    def calculate_pay(self) -> float:
        """重写:固定月薪直接返回"""
        return self.monthly_salary


class HourlyEmployee(Employee):
    def __init__(self, emp_id: str, name: str, hourly_rate: float, hours_worked: float):
        super().__init__(emp_id, name)
        if hourly_rate < 0 or hours_worked < 0:
            raise ValueError("工时或时薪不能为负数")
        self.hourly_rate = float(hourly_rate)
        self.hours_worked = float(hours_worked)

    def calculate_pay(self) -> float:
        """重写:基础工时 + 1.5倍加班工时计算"""
        regular_hours = min(160.0, self.hours_worked)
        overtime_hours = max(0.0, self.hours_worked - 160.0)
        regular_pay = regular_hours * self.hourly_rate
        overtime_pay = overtime_hours * (self.hourly_rate * 1.5)
        return regular_pay + overtime_pay

测试执行与结果输出

# 多态调度统一计算
staff_list = [
    SalariedEmployee("EMP001", "王伟", 15000.0),
    HourlyEmployee("EMP002", "李娜", 80.0, 160.0),   # 标准工时
    HourlyEmployee("EMP003", "张强", 80.0, 180.0),   # 加班20小时: 160*80 + 20*120
]

print("=== 月度薪资结算清单 ===")
for staff in staff_list:
    print(staff.get_details())
# 示例输出:
=== 月度薪资结算清单 ===
[EMP001] 王伟 - 应发薪资: ¥15000.00
[EMP002] 李娜 - 应发薪资: ¥12800.00
[EMP003] 张强 - 应发薪资: ¥15200.00
避坑要点:坚决避免显式硬编码父类

初学者容易写出 Employee.__init__(self, emp_id, name) 这种写法。尽管在简单单继承下可以运行,但一旦未来引入多继承或混入类(Mixin),硬编码父类名会绕开 Python 的 MRO 查找链,导致父类方法被重复调用或漏调用。始终使用 super().__init__() 是遵循现代 Python 规范的最佳实践。

练习题三:多渠道通知服务——多继承与 MRO 解析顺序

多继承允许一个子类融合多个父类的能力,但随之带来的是经典的“菱形继承(Diamond Inheritance)”问题。Python 采用 C3 线性化算法确定方法解析顺序(Method Resolution Order,简称 MRO)。深入理解 MRO 与协同调用(Cooperative Super Call)是掌握高级 OOP 的关键。

题目需求

  1. 设计公共基类 BaseNotifier,包含空方法 send(message)。
  2. 创建两个独立子类:EmailNotifier(处理邮件发送逻辑)与 AuditLogNotifier(将通知记录追加到审计日志中)。两者均需重写 send(message) 并在内部执行 super().send(message)。
  3. 创建终极派生类 AlertService,同时继承 EmailNotifier 与 AuditLogNotifier。
  4. 打印 AlertService.__mro__,观察并验证消息的协同流转顺序。
Python面向对象程序设计练习题:类与继承实战
图 3:多继承菱形拓扑图与 C3 线性化解析路径拓扑图

参考代码实现

class BaseNotifier:
    def send(self, message: str):
        # 终点基类:结束调用链
        print(f"[BaseNotifier] 抵达链条末端,消息入库待命: '{message}'")


class EmailNotifier(BaseNotifier):
    def send(self, message: str):
        print(f"[EmailNotifier] 正在向管理员发送邮件: '{message}'")
        # 协同调用:沿当前实例的 MRO 链条继续向后触发
        super().send(message)


class AuditLogNotifier(BaseNotifier):
    def send(self, message: str):
        print(f"[AuditLogNotifier] 正在写入本地审计日记: '{message}'")
        super().send(message)


class AlertService(EmailNotifier, AuditLogNotifier):
    """告警服务:同时具备邮件告警与审计留痕能力"""
    def send(self, message: str):
        print("[AlertService] 开始执行多渠道协同告警...")
        super().send(message)
        print("[AlertService] 所有通知渠道执行完毕。")

测试执行与结果输出

# 打印 MRO 顺序
print("MRO 链条:")
for idx, cls in enumerate(AlertService.__mro__):
    print(f"  {idx + 1}. {cls.__name__}")

print("\n--- 发送测试 ---")
service = AlertService()
service.send("数据库连接池耗尽警告")
# 示例输出:
MRO 链条:
  1. AlertService
  2. EmailNotifier
  3. AuditLogNotifier
  4. BaseNotifier
  5. object

--- 发送测试 ---
[AlertService] 开始执行多渠道协同告警...
[EmailNotifier] 正在向管理员发送邮件: '数据库连接池耗尽警告'
[AuditLogNotifier] 正在写入本地审计日记: '数据库连接池耗尽警告'
[BaseNotifier] 抵达链条末端,消息入库待命: '数据库连接池耗尽警告'
[AlertService] 所有通知渠道执行完毕。
原理深度解析:super() 究竟是谁的父类?

初学者最大的误区是认为“super() 指向当前类的直接父类”。实际上,super() 绑定的是当前运行实例所在类的 MRO 线性列表中的下一个节点。在 AlertService 的实例中,EmailNotifier.send 中的 super().send 调用的是兄弟类 AuditLogNotifier.send,而非 BaseNotifier。如果其中任何一个子类忘记写 super().send,整个链条就会中断。

练习题四:图形绘制引擎——抽象基类(ABC)与接口契约

在多人协同或框架设计中,通常需要强制派生类实现指定的一组接口。如果基类只写一个普通的空方法或 pass,子类漏写重写方法时只会在运行时崩溃。利用 Python 标准库的 abc 模块,可以在类实例化阶段直接拦截违规代码。

题目需求

  1. 定义抽象基类 Shape(继承自 abc.ABC),声明抽象方法 area() 与 perimeter()。
  2. 实现矩形类 Rectangle(width, height) 与圆形类 Circle(radius),必须完整实现上述两项数学计算。
  3. 编写独立的渲染引擎类 RenderEngine,其 render(shape) 方法要求传入的入参必须是 Shape 实例,并输出图形的面积和周长。

参考代码实现

from abc import ABC, abstractmethod
import math

class Shape(ABC):
    @abstractmethod
    def area(self) -> float:
        """计算图形面积(必须由子类实现)"""
        pass

    @abstractmethod
    def perimeter(self) -> float:
        """计算图形周长(必须由子类实现)"""
        pass


class Rectangle(Shape):
    def __init__(self, width: float, height: float):
        if width <= 0 or height <= 0:
            raise ValueError("矩形边长必须大于0")
        self.width = float(width)
        self.height = float(height)

    def area(self) -> float:
        return self.width * self.height

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


class Circle(Shape):
    def __init__(self, radius: float):
        if radius <= 0:
            raise ValueError("圆形半径必须大于0")
        self.radius = float(radius)

    def area(self) -> float:
        return math.pi * (self.radius ** 2)

    def perimeter(self) -> float:
        return 2 * math.pi * self.radius


class RenderEngine:
    @staticmethod
    def render(shape: Shape):
        # 基于抽象契约实施防御性校验
        if not isinstance(shape, Shape):
            raise TypeError(f"期望 Shape 类型,收到: {type(shape).__name__}")
        print(f"[{shape.__class__.__name__}] 面积: {shape.area():.2f}, 周长: {shape.perimeter():.2f}")

测试执行与结果输出

# 正常调用渲染
engine = RenderEngine()
rect = Rectangle(10.0, 5.0)
circ = Circle(4.0)

engine.render(rect)
engine.render(circ)

# 测试未实现抽象方法的违规情况
class BrokenTriangle(Shape):
    def __init__(self, a, b, c):
        self.a, self.b, self.c = a, b, c
    # 故意遗漏 area 与 perimeter

try:
    t = BrokenTriangle(3, 4, 5)
except TypeError as err:
    print("\n拦截未完整实现的子类实例化:")
    print(err)
# 示例输出:
[Rectangle] 面积: 50.00, 周长: 30.00
[Circle] 面积: 50.27, 周长: 25.13

拦截未完整实现的子类实例化:
Can't instantiate abstract class BrokenTriangle with abstract methods area, perimeter

总结:类与继承实战设计指南

面向对象程序设计并不是继承层级越深越好。在真实工程中,盲目滥用继承容易造成“脆弱基类(Fragile Base Class)”问题。设计面向对象结构时,应当遵循以下判断模型。

Python面向对象程序设计练习题:类与继承实战
图 4:Python 类与继承设计决策指南与实践总结全景图
设计模式/机制 核心语法 / API 典型适用场景 常见工程风险与防范
属性封装 _attr, @property, @setter 需要进行数据范围校验、状态一致性保护的业务对象 禁止直接暴露裸属性写入;避免滥用双下划线名称改写
单继承与多态 class Sub(Base), super() 具备明确“父子/泛化-特化”归属的业务实体 切勿硬编码父类名称;重写方法必须保持入参与返回值类型兼容
协同多继承 super().method(*args), __mro__ 横向功能混入(Mixin)、审计与多渠道协同分发系统 中间类遗漏 super() 导致链条中断;参数签名必须协同透传
抽象基类契约 abc.ABC, @abstractmethod 框架插件系统、图形渲染引擎、算法驱动统一接口 抽象方法不可过多;优先按单一职责原则拆分细粒度接口

掌握类与继承的关键,不在于能否构造多么复杂的继承层级树,而在于能否用最简明的模型表达业务逻辑。在日常编码中,牢记“优先使用组合,谨慎使用继承”,善用 @property 收敛变更入口,并借助 abc.ABC 约束公共接口,便能编写出健壮、易测试且高内聚的 Python 面向对象代码。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表