05-多态
Python 版本要求:Python 3.11+ 贯穿项目:oop_demo/ 代码位置:
oop_demo/app/ports/catalog.py+oop_demo/app/domain/book/model.py测试验证:cd oop_demo && uv run pytest -k polymorph -v
概念铺垫
为什么需要多态?
问题场景
图书馆系统里有三种图书:实体书、电子书、有声书。每本书都要展示编目信息,但每种书的"关键信息"不同:
| 图书类型 | get_info() 输出示例 |
|---|---|
| PhysicalBook | `... |
| EBook | `... |
| AudioBook | `... |
不使用多态:需要逐一 isinstance 判断,每加一种新类型就要改一遍函数。
使用多态:统一调用 book.get_info(),Python 自动根据实际类型选择正确的实现。
isinstance 的场景
除了 get_info() 多态调用,还需要判断对象是否"可借阅"。传统做法是 isinstance(book, BookItem),但如果有一个外部类也实现了借还逻辑呢?Protocol 让 isinstance 只看行为,不看继承。
生活类比:万能遥控器
同一个"开/关"按钮(接口),能控制电视、空调、音响(不同实现)。用户不需要知道每种设备的内部电路,只需要按同一个按钮。
- 按钮 = 接口:
get_info(),checkout(),return_item() - 不同设备 = 不同类:PhysicalBook, EBook, AudioBook
- 用户 = 调用者:
for book in books: print(book.get_info())
L1 理解层:会用
多态的核心:同一接口,不同实现
多态的三种方式
┌──────────────────┬────────────────────┬────────────────────────┐
│ 方式 │ 匹配机制 │ 适用关系 │
├──────────────────┼────────────────────┼────────────────────────┤
│ 1. 继承多态 │ 子类重写父类方法 │ is-a(明确的继承关系) │
│ 2. ABC 多态 │ 抽象基类强制实现 │ is-a(必须遵守的契约) │
│ 3. Protocol 多态 │ 结构匹配(鸭子类型) │ has-behavior(有这行为就行)│
└──────────────────┴────────────────────┴────────────────────────┘ABC(抽象基类):Catalogable
概念
ABC(Abstract Base Class)用于定义"必须遵守的契约":
- 继承 ABC 的类不能直接实例化,除非实现了所有
@abstractmethod - 未实现抽象方法的子类在实例化时立即抛出
TypeError - 适用于 is-a 关系:PhysicalBook 是一种 可编目的图书
代码:ports/catalog.py
from abc import ABC, abstractmethod
class Catalogable(ABC):
"""可被编目的抽象基类
所有图书类型都必须实现 get_info() 和 get_isbn(),
否则无法实例化 → 演示 ABC 的强制接口约束。
"""
@abstractmethod
def get_info(self) -> str:
...
@abstractmethod
def get_isbn(self) -> str:
...子类实现:domain/book/model.py
class BookItem(Catalogable):
"""图书基类 — 显式继承 Catalogable"""
def get_isbn(self) -> str:
return self._isbn
def get_info(self) -> str:
return f"[{self._isbn}] {self.title} — {self.author} ({self._year})"
class PhysicalBook(BookItem):
"""实体书 — 重写 get_info(),添加页数信息"""
def get_info(self) -> str:
return f"{super().get_info()} | {self._pages} 页"
class EBook(BookItem):
"""电子书 — 重写 get_info(),添加格式和大小"""
def get_info(self) -> str:
return f"{super().get_info()} | {self._format} {self.__file_size_mb:.1f} MB"
class AudioBook(PhysicalBook, DigitalMixin):
"""有声书 — 重写 get_info(),添加有声书标识"""
def get_info(self) -> str:
return f"{super().get_info()} | 有声书 {self.download_info()}"ABC 的强制约束
>>> class Incomplete(Catalogable):
... pass # 未实现 get_info() 和 get_isbn()
>>> Incomplete()
TypeError: Can't instantiate abstract class Incomplete
without an implementation for abstract methods 'get_info', 'get_isbn'关键理解:错误发生在实例化时(
Incomplete()),而非定义时。Python 在__new__阶段检查抽象方法是否全部实现。
Protocol(结构类型):Borrowable
概念
Protocol 用于定义"行为协议"——不关心你继承什么,只看你有没有这些方法:
- 无需显式继承:只要对象拥有协议要求的方法,就被视为满足协议
- 结构子类型(Structural Subtyping):匹配基于结构,而非名义
- 适用于 has-behavior 关系:有借还行为的对象就是可借阅的
@runtime_checkable让isinstance(obj, Protocol)在运行时生效
代码:ports/catalog.py
from typing import Protocol, runtime_checkable
@runtime_checkable
class Borrowable(Protocol):
"""可借阅协议 — 鸭子类型
只要对象拥有这三个方法,就被视为"可借阅",无需显式继承。
@runtime_checkable 允许 isinstance() 检查。
"""
def checkout(self) -> bool:
...
def return_item(self) -> None:
...
def is_available(self) -> bool:
...结构匹配:自动满足
from app.domain.book.model import BookItem
from app.ports.catalog import Borrowable
book = BookItem("测试", "作者", "9780000000001", 2020)
isinstance(book, Borrowable) # True — BookItem 没有显式继承 Borrowable
# 但实现了 checkout/return_item/is_available关键理解:BookItem 从未声明
class BookItem(Borrowable),但因为实现了三个方法,Python 在isinstance时自动认为它满足 Borrowable 协议。这就是"结构子类型"——看结构,不看声明。
ABC vs Protocol 对比
这是学习者最常混淆的问题。核心区别在于匹配机制:
| 特性 | ABC(Catalogable) | Protocol(Borrowable) |
|---|---|---|
| 继承要求 | 必须显式继承 Catalogable | 无需继承,自动匹配 |
| 检查方式 | 名义子类型(Nominal) | 结构子类型(Structural) |
| 适用关系 | is-a(是一种) | has-behavior(有这行为) |
| 实例化保护 | 未实现抽象方法 → TypeError | 不保护,运行时才发现缺方法 |
| isinstance | 直接支持 | 需要 @runtime_checkable 装饰器 |
| 类型检查器 | mypy 基于继承关系检查 | mypy 基于结构匹配检查 |
| 典型场景 | 强制子类实现特定接口 | 灵活接纳第三方对象 |
何时用 ABC
# 场景:你是框架作者,要求所有插件必须实现特定方法
class Plugin(ABC):
@abstractmethod
def execute(self, data: dict) -> dict: ...
# 好处:如果开发者忘了实现,实例化时立即报错,不会等到运行时何时用 Protocol
# 场景:你写了一个函数,希望接受任何有 .read() 方法的对象
class Readable(Protocol):
def read(self, size: int = -1) -> str: ...
def process(reader: Readable) -> str:
return reader.read()
# 好处:标准库的 open()、io.StringIO()、自定义类——只要实现了 read() 就能用
# 无需修改第三方类的继承关系选择决策树
需要强制子类实现方法?
├── 是 → ABC(契约式,失败快)
└── 否 → 需要类型提示 + 灵活性?
├── 是 → Protocol(结构匹配,鸭子类型 + 静态检查)
└── 否 → 直接用鸭子类型(不写 Protocol,纯运行时)贯穿实战:多态调用
demo_ch05 演示(app/main.py)
def demo_ch05() -> None:
"""多态 — ABC + Protocol"""
from app.domain.book.model import AudioBook, BookItem, EBook, PhysicalBook
from app.ports.catalog import Borrowable, Catalogable
# 1. ABC 强制约束演示
try:
class Incomplete(Catalogable):
pass
Incomplete()
except TypeError as e:
print(f"实例化未实现抽象方法的类 → TypeError: {e}")
# 2. Protocol 结构匹配演示
book = BookItem("测试", "作者", "9780000000001", 2020)
print(f"BookItem 是 Borrowable? {isinstance(book, Borrowable)}")
# 3. 多态调用:同一方法,不同实现
books: list[Catalogable] = [
PhysicalBook("流畅的Python", "Ramalho", "9781491946008", 2015, 792),
EBook("深入理解Python", "作者", "9780000000002", 2022, 5.2, "EPUB"),
AudioBook("代码大全", "McConnell", "9780735619678", 2004, 960, 150.0),
]
for b in books:
print(f" {b.__class__.__name__}: {b.get_info()}")运行验证
# 方式 1:交互式菜单
cd oop_demo && uv run python -m app # 选 5
# 方式 2:单元测试
cd oop_demo && uv run pytest -k polymorph -v测试覆盖(tests/test_oop.py:TestPolymorphism)
| 测试 | 验证内容 |
|---|---|
test_catalogable_abc_cannot_instantiate_without_methods | ABC 强制约束 |
test_book_item_satisfies_catalogable | BookItem 满足 Catalogable |
test_book_item_satisfies_borrowable_protocol | BookItem 满足 Borrowable(结构匹配) |
test_borrowable_checkout_return_cycle | 借还状态循环 |
test_polymorphic_get_info | 多态调用返回不同格式 |
L2 实践层:用好
推荐做法
| 做法 | 原因 | 示例 |
|---|---|---|
| is-a 关系用 ABC | 强制契约,失败快 | class BookItem(Catalogable) |
| has-behavior 用 Protocol | 灵活接纳,无需修改第三方 | isinstance(obj, Borrowable) |
Protocol 加 @runtime_checkable | 否则 isinstance 会报错 | @runtime_checkable class Borrowable(Protocol) |
| 类型提示用基类或 Protocol | 不要写 list[object] | books: list[Catalogable] |
ABC 至少有一个 @abstractmethod | 否则 ABC 可被实例化 | Catalogable 有 2 个抽象方法 |
| 避免 isinstance 分支判断 | 违反开放封闭原则 | 直接用多态调用 |
反模式:不要这样做
# ❌ 用 isinstance 分支判断类型 —— 违反开放封闭原则
def show_book_info(book):
if isinstance(book, PhysicalBook):
print(f"{book.title} — {book.pages}页")
elif isinstance(book, EBook):
print(f"{book.title} — {book.file_size_mb}MB")
# ✅ 用多态调用 —— 新增类型无需修改
def show_book_info(book: Catalogable) -> None:
print(book.get_info())# ❌ ABC 接口过大 — 子类被迫实现不需要的方法
class LibraryItem(ABC):
@abstractmethod
def checkout(self): ...
@abstractmethod
def return_item(self): ...
@abstractmethod
def download(self): ... # 纸质书不需要!
@abstractmethod
def scan_barcode(self): ... # 电子书不需要!
# ✅ 拆分小接口
class Borrowable(Protocol):
def checkout(self) -> bool: ...
def return_item(self) -> None: ...
class Downloadable(Protocol):
def download(self) -> bool: ...# ❌ Protocol 忘加 @runtime_checkable
class MyProto(Protocol):
def do_thing(self): ...
isinstance(obj, MyProto) # TypeError: Instance and class checks can only be used
# with @runtime_checkable protocols
# ✅ 需要运行时检查必须加装饰器
@runtime_checkable
class MyProto(Protocol):
def do_thing(self): ...适用场景
| 场景 | 是否推荐 | 原因 |
|---|---|---|
| 框架定义插件接口 | ABC | 强制契约,实例化时报错 |
| 接受第三方对象 | Protocol | 不要求修改继承关系 |
| 简单的"有方法就能用" | 鸭子类型(不写 Protocol) | 过度设计 |
| 需要静态类型检查 | Protocol | mypy 支持结构子类型 |
| 需要运行时检查 | ABC 或 @runtime_checkable Protocol | isinstance 检查 |
常见陷阱
| 陷阱 | 问题 | 解决方案 |
|---|---|---|
忘记 @abstractmethod | ABC 可被实例化,失去保护作用 | 每个必须实现的方法都加装饰器 |
Protocol 忘加 @runtime_checkable | isinstance(obj, Protocol) 抛 TypeError | 需要运行时检查时加上装饰器 |
isinstance 替代多态 | 写成 if isinstance(x, A): ... elif isinstance(x, B): ... | 直接用多态调用 x.do_thing() |
| ABC 接口过大 | 子类被迫实现不需要的方法 | 拆分为小 ABC 或用 Protocol |
| Protocol 方法签名不一致 | 结构匹配只看方法名和参数数量 | 确保方法签名与协议一致 |
| 混用 ABC 和 Protocol 做同一件事 | 增加理解成本 | 按 is-a vs has-behavior 二选一 |
L3 专家层:深入
Python 如何实现:鸭子类型与虚子类机制
CPython 中的鸭子类型本质
Python 的鸭子类型不依赖接口声明,而是依赖运行时的属性查找。当你写 book.checkout() 时:
book.checkout()
│
▼
┌───────────────────────────────────────────────┐
│ CPython LOAD_METHOD + CALL_METHOD 字节码 │
│ │
│ 1. 在 type(book).__mro__ 中查找 'checkout' │
│ 2. 如果找到且是描述符 → 调用 __get__ │
│ 3. 如果找到且是普通函数 → 绑定 self │
│ 4. 调用返回的 callable │
│ │
│ 关键是:从不检查 book 的类型是否匹配任何接口 │
│ 只要对象有该方法,就能调用 —— 这就是鸭子类型 │
└───────────────────────────────────────────────┘ABC 虚子类注册机制
CPython 的 ABC 模块允许通过 register() 方法将任意类注册为虚子类:
from abc import ABC, abstractmethod
class MyABC(ABC):
@abstractmethod
def my_method(self): ...
# 一个已有类,完全不继承 MyABC
class ExistingClass:
def my_method(self):
return "I work!"
# 注册为虚子类
MyABC.register(ExistingClass)
# 现在 isinstance 返回 True!
obj = ExistingClass()
print(isinstance(obj, MyABC)) # True
print(issubclass(ExistingClass, MyABC)) # True虚子类注册的内部机制:
MyABC.register(ExistingClass)
│
▼
┌──────────────────────────────────────────────────┐
│ ABCMeta.__subclasscheck__ 被调用 │
│ │
│ 检查逻辑(简化): │
│ 1. 检查 ExistingClass 是否在 MyABC 的 _abc_registry 中
│ 2. 如果是 → 返回 True(直接注册的虚子类) │
│ 3. 检查 ExistingClass 是否继承了 MyABC │
│ 4. 如果是 → 返回 True(名义子类) │
│ 5. 否则 → 返回 False │
│ │
│ 关键:ABCMeta 缓存 __subclasscheck__ 的结果 │
│ 每次 isinstance() 调用时优先查缓存 │
└──────────────────────────────────────────────────┘# 验证:查看 ABC 的内部注册表
from abc import ABC
class MyABC(ABC):
pass
class MyClass:
pass
MyABC.register(MyClass)
# 查看内部缓存
print(MyABC._abc_impl)
# <_abc_data object at 0x...> (C 级别的缓存,包含注册的虚子类)
# 查看注册表
print(MyABC._abc_registry)
# <_weakrefset.WeakSet object at 0x...>
# 注意:_abc_registry 使用弱引用,防止内存泄漏@runtime_checkable Protocol 的 instancecheck
# @runtime_checkable 的等效实现原理
class Borrowable(Protocol):
def checkout(self) -> bool: ...
def return_item(self) -> None: ...
def is_available(self) -> bool: ...
# @runtime_checkable 做了两件事:
# 1. 将 Borrowable.__instancecheck__ 指向一个自定义实现
# 2. 该实现检查对象是否拥有协议要求的所有方法
# 等效于:
def __instancecheck__(cls, instance):
# 检查 instance 是否有协议定义的所有方法名
required_methods = {'checkout', 'return_item', 'is_available'}
for name in required_methods:
if not hasattr(instance, name):
return False
# 可选:检查方法签名
return True# 验证:Protocol 的结构匹配机制
from typing import Protocol, runtime_checkable
@runtime_checkable
class Drawable(Protocol):
def draw(self) -> None: ...
class Circle:
def draw(self) -> None:
print("drawing circle")
class Square:
def draw(self) -> None:
print("drawing square")
class NoDraw:
pass
print(isinstance(Circle(), Drawable)) # True — 有 draw() 方法
print(isinstance(Square(), Drawable)) # True — 有 draw() 方法
print(isinstance(NoDraw(), Drawable)) # False — 没有 draw() 方法性能考量
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| ABC 实例化检查 | O(n × m) | n=待检查方法数,m=抽象方法数,有缓存 |
isinstance(obj, ABC) | O(1) | ABC 有 __subclasscheck__ 缓存 |
isinstance(obj, Protocol) | O(k) | k=协议方法数,逐个检查 hasattr |
虚子类注册 ABC.register() | O(1) | 加入弱引用集合 |
| 多态方法调用 | O(1) | 正常属性查找,无额外开销 |
# 性能对比:isinstance 检查
import timeit
from abc import ABC
from typing import Protocol, runtime_checkable
class MyABC(ABC):
pass
@runtime_checkable
class MyProto(Protocol):
def method(self): ...
class C(MyABC):
def method(self): pass
# ABC isinstance 检查(有缓存)
print(timeit.timeit("isinstance(c, MyABC)", globals={'c': C(), 'MyABC': MyABC}, number=1000000))
# ~0.05 秒
# Protocol isinstance 检查(需遍历方法)
print(timeit.timeit("isinstance(c, MyProto)", globals={'c': C(), 'MyProto': MyProto}, number=1000000))
# ~0.15 秒(比 ABC 慢约 3 倍,但不影响实际使用)知识关联
多态知识关联:
┌───────────────┐
│ 第 3 章:继承 │
│ 重写/扩展 │
└───────┬───────┘
│
┌────────────┼────────────┐
↓ ↓ ↓
┌───────────┐ ┌───────────┐ ┌───────────┐
│ 继承多态 │ │ ABC 多态 │ │Protocol多态│
│ 子类重写 │ │ 强制契约 │ │ 结构匹配 │
└───────────┘ └───────────┘ └───────────┘
│ │
↓ ↓
┌───────────────┐ ┌─────────────┐
│ 设计原则(第6章)│ │ 魔术方法(第8章)│
│ LSP / DIP │ │ __eq__/__lt__│
└───────────────┘ └─────────────┘
│
↓
┌───────────────┐
│ ABCMeta │
│ __subclass │ ← 虚子类注册机制
│ check__ │ 第 10 章详解
└───────────────┘自检清单
回答以下问题,检验理解程度:
为什么
Incomplete(Catalogable)定义时不报错,实例化时才报错? → Python 的 ABC 在__new__阶段检查抽象方法,定义类时不检查,实例化时才检查。BookItem 没有继承 Borrowable,为什么
isinstance(book, Borrowable)返回 True? → Borrowable 是@runtime_checkable的 Protocol,isinstance 只看结构(是否有 checkout/return_item/is_available),不看继承。如果给 EBook 删除
get_info()方法,会发生什么? → 实例化 EBook 时抛TypeError,因为它继承了 Catalogable(通过 BookItem),必须实现所有抽象方法。什么时候该用 ABC,什么时候该用 Protocol? → 需要"强制子类实现"时用 ABC(契约式);需要"只要有这行为就行"时用 Protocol(结构式)。
books: list[Catalogable] = [PhysicalBook, EBook, AudioBook]的类型提示有什么作用? → 告诉阅读者和类型检查器:这个列表里的元素都保证有get_info()和get_isbn(),可以安全调用。
本章能力清单
完成本章后,你应该能够:
- [ ] 解释多态的核心概念:同一接口,不同实现
- [ ] 使用 ABC 定义抽象基类,强制子类实现特定方法
- [ ] 使用 Protocol 定义行为协议,实现结构子类型
- [ ] 正确使用
@runtime_checkable使 Protocol 支持isinstance检查 - [ ] 根据 is-a vs has-behavior 选择 ABC 或 Protocol
- [ ] 在子类中重写父类/基类方法,使用
super()调用链 - [ ] 编写多态调用代码,避免
isinstance分支判断 - [ ] 识别并避免多态常见陷阱(接口过大、签名不一致等)