继承和多态
继承让一个类复用并扩展另一个类的行为;多态让调用方只依赖共同接口,而不必关心对象的具体类型。两者结合后,可以用同一段代码处理不同对象。
本文示例适用于 Python 3.10 及以上版本。学完后,你应该能够:
- 定义父类和子类,并正确调用父类初始化逻辑;
- 覆盖方法、使用
super()和理解方法解析顺序; - 通过继承、鸭子类型或抽象基类实现多态;
- 判断什么时候应该用继承,什么时候应该用组合。
从一个多态例子开始
class Animal:
def speak(self) -> str:
raise NotImplementedError
class Dog(Animal):
def speak(self) -> str:
return "汪汪"
class Cat(Animal):
def speak(self) -> str:
return "喵喵"
def let_them_speak(animals: list[Animal]) -> None:
for animal in animals:
print(animal.speak())
let_them_speak([Dog(), Cat()])
let_them_speak() 只要求对象有 speak() 行为,不需要写 if type(...) is Dog。同一调用可以处理 Dog、Cat 以及以后新增的其他动物,这就是多态的价值。
定义继承关系
在 class Child(Parent): 中,括号里的类是父类:
class User:
def __init__(self, name: str):
self.name = name
def introduce(self) -> str:
return f"我叫{self.name}。"
class Admin(User):
def __init__(self, name: str, level: int = 1):
super().__init__(name)
self.level = level
def reset_password(self, user: User) -> None:
print(f"管理员 {self.name} 重置了 {user.name} 的密码")
子类自动获得父类的属性和方法:
admin = Admin("小林", 2)
print(admin.introduce()) # 我叫小林。
print(isinstance(admin, User)) # True
print(issubclass(Admin, User)) # True
覆盖初始化方法
子类一旦定义自己的 __init__(),父类的 __init__() 不会自动执行。需要父类状态时,应调用 super().__init__(...):
class PremiumAdmin(Admin):
def __init__(self, name: str, level: int = 1, expires_in_days: int = 30):
super().__init__(name, level)
self.expires_in_days = expires_in_days
忘记调用 super() 可能导致父类属性不存在,直到某个方法使用它时才出现 AttributeError。
覆盖普通方法
子类可以提供同名方法改变行为:
class Admin(User):
def introduce(self) -> str:
return f"{super().introduce()} 我是管理员。"
super() 表示按照方法解析顺序寻找下一个实现。它不是简单地“调用写在括号里的父类”,在多继承中也会按照 MRO 协作调用。
多态的三种常见形式
继承多态
父类定义共同接口,子类分别实现:
class Shape:
def area(self) -> float:
raise NotImplementedError
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return 3.141592653589793 * self.radius**2
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
def total_area(shapes: list[Shape]) -> float:
return sum(shape.area() for shape in shapes)
调用方依赖 area() 这一共同约定,而不是依赖具体类名。
鸭子类型
Python 通常不要求对象必须继承某个父类。只要对象支持所需操作,就可以使用,这叫“鸭子类型”:
class Report:
def write(self, text: str) -> None:
print(f"报告:{text}")
class Console:
def write(self, text: str) -> None:
print(f"控制台:{text}")
def output(writer, text: str) -> None:
writer.write(text)
output(Report(), "完成")
output(Console(), "完成")
这段代码没有检查 writer 的类型,只使用它的 write() 方法。优点是灵活,缺点是接口错误通常在运行时才发现。类型注解可以帮助编辑器和类型检查器尽早提示问题。
抽象基类
需要明确声明“子类必须实现哪些方法”时,可以使用 abc:
from abc import ABC, abstractmethod
class Payment(ABC):
@abstractmethod
def pay(self, amount: float) -> str:
"""完成支付并返回交易编号。"""
class CardPayment(Payment):
def pay(self, amount: float) -> str:
return f"card:{amount:.2f}"
# Payment() # TypeError:不能实例化含抽象方法的类
print(CardPayment().pay(10))
抽象基类适合插件、驱动和大型团队协作等需要显式契约的场景。简单脚本通常使用鸭子类型就足够。
子类型应该遵守父类契约
如果代码接受 Animal,传入任何子类都应该能正常工作。子类可以加强实现,但不应悄悄改变父类方法的基本约定:
- 参数含义和可接受范围不能无故变窄;
- 返回值应满足调用方预期;
- 父类承诺的行为不能被子类破坏;
- 抛出的异常应当有清晰、可预期的边界。
例如,父类的 withdraw() 允许取款金额为正数,子类不应突然要求金额必须是偶数,否则调用父类接口的代码可能失效。这种可替换性通常称为里氏替换原则。
多继承、MRO 和 mixin
Python 支持多继承:
class JsonMixin:
def to_json(self) -> str:
import json
return json.dumps(self.__dict__, ensure_ascii=False)
class User:
def __init__(self, name: str):
self.name = name
class SerializableUser(JsonMixin, User):
pass
user = SerializableUser("小林")
print(user.to_json())
print(SerializableUser.__mro__)
__mro__ 展示方法解析顺序。super() 会沿着这条顺序寻找下一个实现,因此多个父类需要协作时,每个实现都应使用 super(),不要直接写死某个父类名称。
Mixin 通常只提供一组小而独立的能力,不代表完整的业务实体。混入类应尽量少保存状态,避免多继承层次过深和方法名冲突。
继承还是组合
继承表达“是一种(is-a)”关系:管理员是一种用户,圆形是一种形状。组合表达“拥有一个(has-a)”关系:订单拥有支付方式。
class Logger:
def info(self, message: str) -> None:
print(message)
class OrderService:
def __init__(self, logger: Logger):
self.logger = logger
def create(self) -> None:
self.logger.info("订单已创建")
组合可以在运行时替换组件,耦合更低,通常比为了复用几行代码而继承更灵活。当两个类只有少量实现相同、概念却不满足“是一种”时,优先考虑组合或独立函数。
常见错误
用类型判断代替多态
def bad_area(shape):
if isinstance(shape, Circle):
return shape.radius * shape.radius * 3.141592653589793
if isinstance(shape, Rectangle):
return shape.width * shape.height
raise TypeError("未知图形")
新增图形时必须修改 bad_area()。更好的做法是让每种图形实现 area(),调用方直接调用共同接口。
覆盖方法时忘记父类状态
覆盖 __init__() 后漏掉 super() 是最常见问题之一。可以写测试验证父类定义的重要属性在子类实例中仍然存在。
继承层次过深
五六层以上的继承关系通常难以追踪方法来自哪里。把变化点拆成组合对象、策略函数或小型 mixin,往往更容易测试和替换。
练习
练习一:通知渠道
定义 Notifier 抽象基类和 EmailNotifier、SmsNotifier 两个实现。编写 send_all(notifiers, message),让它不判断具体类型即可发送通知。
练习二:导出器
实现 CsvExporter 和 JsonExporter,都提供 export(rows) 方法。尝试让一个 ReportService 通过组合接收任意导出器。
练习三:协作式多继承
定义两个 mixin,分别提供日志和 JSON 能力。让业务类通过 super() 正确初始化,并打印 __mro__ 解释调用顺序。
小结
class Child(Parent)建立继承关系,子类可以复用和覆盖父类行为;- 覆盖
__init__()后,必要时用super().__init__()初始化父类状态; - 多态让调用方依赖共同接口,而不是依赖具体类型;
- 鸭子类型强调“能做什么”,抽象基类则显式声明“必须实现什么”;
- 子类应遵守父类契约,保证可以被父类接口替换;
- 多继承依赖 MRO 和协作式
super(),Mixin 应保持小而独立; - “是一种”适合继承,“拥有一个”通常适合组合。