Python面向对象程序设计练习题:类与继承实战
Python面向对象程序设计(OOP)的核心在于将现实问题抽象为封装的数据与行为,并通过继承与多态实现逻辑复用与扩展。面向对象的语法虽然简洁,但在实际工程开发中,开发者常常面临“如何合理划分属性封装边界”、“何时使用继承而非组合”、“super() 在复杂继承下的调用链条”等具体挑战。本文精选四个层层递进的经典实战练习题,分别涵盖基础类封装、单继承与方法重写、多继承与 MRO 解析、抽象基类与接口契约设计。每个题目均包含完整的场景需求、工程化实现代码、执行结果说明及易错点剖析,帮助读者建立严谨的面向对象设计思维。
练习题一:电商库存管理——类的封装与属性控制
在电商系统中,直接暴露对象属性容易引发脏数据写入风险。例如负库存、非法价格变动等操作可能破坏业务数据一致性。合理的做法是将关键数据私有化或保护化,仅通过定义明确的属性装饰器与方法对外提供受控访问。
题目需求
- 设计
Product商品类,初始化时接收商品名称name、单价price与初始库存stock。 name一经初始化不可变更;price必须为大于 0 的正数;stock必须为大于等于 0 的整数。- 提供
restock(quantity)补货方法与deduct(quantity)出库扣减方法,出库数量大于现有库存时抛出异常并阻止扣减。
参考代码实现
# 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() 是保证初始化链路完整执行的核心。
题目需求
- 设计基类
Employee,统一维护工号emp_id和姓名name,并提供默认的月度薪资计算方法calculate_pay()。 - 创建派生类
SalariedEmployee(月薪制员工),增加基础月薪属性monthly_salary。 - 创建派生类
HourlyEmployee(时薪制员工),增加时薪hourly_rate与当月工时hours_worked,超过 160 小时的部分按 1.5 倍加班费核算。 - 两个子类均需重写
calculate_pay(),并在上层以列表形式统一批量遍历结算。
参考代码实现
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 的关键。
题目需求
- 设计公共基类
BaseNotifier,包含空方法send(message)。 - 创建两个独立子类:
EmailNotifier(处理邮件发送逻辑)与AuditLogNotifier(将通知记录追加到审计日志中)。两者均需重写send(message)并在内部执行super().send(message)。 - 创建终极派生类
AlertService,同时继承EmailNotifier与AuditLogNotifier。 - 打印
AlertService.__mro__,观察并验证消息的协同流转顺序。
参考代码实现
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() 绑定的是当前运行实例所在类的 MRO 线性列表中的下一个节点。在 AlertService 的实例中,EmailNotifier.send 中的 super().send 调用的是兄弟类 AuditLogNotifier.send,而非 BaseNotifier。如果其中任何一个子类忘记写 super().send,整个链条就会中断。
练习题四:图形绘制引擎——抽象基类(ABC)与接口契约
在多人协同或框架设计中,通常需要强制派生类实现指定的一组接口。如果基类只写一个普通的空方法或 pass,子类漏写重写方法时只会在运行时崩溃。利用 Python 标准库的 abc 模块,可以在类实例化阶段直接拦截违规代码。
题目需求
- 定义抽象基类
Shape(继承自abc.ABC),声明抽象方法area()与perimeter()。 - 实现矩形类
Rectangle(width, height)与圆形类Circle(radius),必须完整实现上述两项数学计算。 - 编写独立的渲染引擎类
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)”问题。设计面向对象结构时,应当遵循以下判断模型。
| 设计模式/机制 | 核心语法 / API | 典型适用场景 | 常见工程风险与防范 |
|---|---|---|---|
| 属性封装 | _attr, @property, @setter |
需要进行数据范围校验、状态一致性保护的业务对象 | 禁止直接暴露裸属性写入;避免滥用双下划线名称改写 |
| 单继承与多态 | class Sub(Base), super() |
具备明确“父子/泛化-特化”归属的业务实体 | 切勿硬编码父类名称;重写方法必须保持入参与返回值类型兼容 |
| 协同多继承 | super().method(*args), __mro__ |
横向功能混入(Mixin)、审计与多渠道协同分发系统 | 中间类遗漏 super() 导致链条中断;参数签名必须协同透传 |
| 抽象基类契约 | abc.ABC, @abstractmethod |
框架插件系统、图形渲染引擎、算法驱动统一接口 | 抽象方法不可过多;优先按单一职责原则拆分细粒度接口 |
掌握类与继承的关键,不在于能否构造多么复杂的继承层级树,而在于能否用最简明的模型表达业务逻辑。在日常编码中,牢记“优先使用组合,谨慎使用继承”,善用 @property 收敛变更入口,并借助 abc.ABC 约束公共接口,便能编写出健壮、易测试且高内聚的 Python 面向对象代码。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









