跳到主要内容

继承和多态

继承让一个类复用并扩展另一个类的行为;多态让调用方只依赖共同接口,而不必关心对象的具体类型。两者结合后,可以用同一段代码处理不同对象。

本文示例适用于 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 应保持小而独立;
  • “是一种”适合继承,“拥有一个”通常适合组合。