作者: andylin02
学习章节: 第 13 章 接口:从协议到抽象基类
关键词: 接口|协议|抽象基类|ABC|鸭子类型|白鹅类型|virtual subclass|虚拟子类|__subclasshook__|运行时检查|collections.abc|Goose Typing
一、本章概述
《流畅的 Python》第 13 章系统梳理了 Python 从“协议”到“抽象基类”的接口演化三阶段:动态协议(Duck Typing)→ 静态协议(Static Protocol/Goose Typing)→ 抽象基类(ABC)。本章内容相当于第 12 章“序列协议”之后的一个升级——如果说第 12 章告诉你“如何让对象像序列一样工作”,那么第 13 章教你的则是“如何在多个开发者协作环境中,为‘像序列一样工作’这类概念建立正式的契约”。
接口是实现特定角色的方法集合。
在 Python 中,接口的定义和使用方式有四种类型。它们可以用图 13-1 中的类型图来描述:
- 动态协议(Dynamic Protocol):Duck Typing,依赖对象是否能响应特定方法(无须显式继承)。
- 静态协议(Static Protocol / Goose Typing):Python 3.8+ 引入,通过 typing.Protocol 显式定义,用于静态类型检查器。
- 抽象基类(ABC,Abstract Base Class):Python 2.6+ 通过 abc 模块支持,可在运行时检查类是否符合接口。
- 常规静态类型(Regular Static Types):类型提示中的具体类型,例如 int | float。
第 13 章的核心议题包括:
- 鸭子类型与协议:为什么“len() 函数能作用于某个对象”并不需要这个对象显式实现某个接口?
- 抽象基类(ABC)的工作原理:如何定义 ABC,如何实现虚拟子类(virtual subclass),如何利用 __subclasshook__ 让 isinstance() 和 issubclass() 支持结构检查。
- 白鹅类型(Goose Typing) :ABC 在企业级 Python 编程中的实际应用。
- collections.abc 模块:标准库中内置的常用 ABC。
二、接口的两种定义方式
2.1 非正式接口:运行时协议
Python 的协议只在文档和代码中定义,不在语言层面施加限制。例如,序列协议仅需 __len__ 和 __getitem__ 两个方法被实现。这意味着 Python 中的“接口”是隐式的、非正式的。
2.2 正式接口:抽象基类
从 Python 3.8 开始,有四种方式定义和使用接口。抽象基类是一种方法,它允许开发者定义必须被实现的抽象方法。
接口方法对比
| 动态协议(Duck Typing) | 运行时(属性缺失时报 AttributeError) | 隐式 | 动态灵活,快速迭代 |
| 静态协议(Static Protocol) | 静态类型检查(mypy 等) | 显式 | 类型安全,大型项目 |
| 抽象基类(ABC) | 运行时(isinstance() 等) | 显式 | 框架设计,注册虚拟子类 |
三、鸭子类型:动态协议的基石
“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”
3.1 序列协议示例
class FrenchDeck:
ranks = [str(n) for n in range(2, 11)] + list('JQKA')
suits = 'spades diamonds clubs hearts'.split()
def __init__(self):
from collections import namedtuple
Card = namedtuple('Card', ['rank', 'suit'])
self._cards = [Card(rank, suit) for suit in self.suits for rank in self.ranks]
def __len__(self):
return len(self._cards)
def __getitem__(self, position):
return self._cards[position]
deck = FrenchDeck()
print(len(deck)) # 52
print(deck[0]) # Card(rank='2', suit='spades')
for card in deck[:3]: # 支持切片和迭代!
print(card)
尽管 FrenchDeck 没有继承任何序列基类,但它只需实现 __len__ 和 __getitem__ 方法,Python 就会自动将其视为一个序列。这背后的原理是:Python 解释器在尝试迭代时,会先查找 __iter__ 方法,如果找不到,就回退到通过递增索引反复调用 __getitem__ 直到抛出 IndexError。因此,即使不显式实现 __iter__ 方法,它也能被迭代。
3.2 动态协议的优缺点
优点:
- 高度灵活,无须继承特定的基类
- 代码量小,便于快速开发
- 天然适合临时对象或适配器场景
缺点:
- 隐式约定在大型项目中容易导致难以追踪的 bug
- IDE 无法提供智能提示
- 错误信息的排查成本较高
四、抽象基类:正式接口的运行时检查
Python 的抽象基类主要定义在 collections.abc 模块中,也可通过 abc 模块自行定义。
4.1 核心作用
4.2 ABC 在数字类型中的应用
在 Python 的类型系统中,numbers 模块提供了清晰的抽象基类层次。对数字进行抽象类型检查时,最好使用 numbers.Integral 和 numbers.Real 等,而不是 int 和 float 的组合。
import numbers
def do_math(n: numbers.Real) –> numbers.Real:
"""接受任何名义上的实数"""
return n * 2
print(do_math(5)) # 10
print(do_math(3.14)) # 6.28
# print(do_math("hello")) # mypy 报错,运行时若无检查也会出错
这种做法比针对特定类型进行检查的灵活性高得多,因为 Complex、Real、Rational、Integral 提供了完备的类型层次(标准库允许 decimal.Decimal 以及分数 Fraction 等也纳入该体系)。
4.3 colletions.abc 模块的主要抽象基类
collections.abc 模块提供了常用抽象基类:
| Container | __contains__ | — |
| Hashable | __hash__ | — |
| Iterable | __iter__ | — |
| Sized | __len__ | — |
| Sequence | __len__, __getitem__ | __contains__, __iter__, __reversed__, index, count |
| MutableSequence | __len__, __getitem__, __setitem__, __delitem__, insert | 继承 Sequence 的所有方法 + append, reverse, extend, pop, remove, iadd |
| Set | __len__, __iter__, __contains__ | 集合运算符(&, ` |
| MutableSet | __len__, __iter__, __contains__, add, discard | 继承 Set 的所有方法 |
| Mapping | __getitem__, __len__, __iter__ | __contains__, keys, values, items, get, __eq__, __ne__ |
| MutableMapping | __getitem__, __setitem__, __delitem__, __len__, __iter__ | 继承 Mapping 的所有方法 + pop, popitem, clear, update, setdefault |
4.4 使用 ABC 创建自定义抽象基类
abc 模块允许定义抽象基类,抽象方法使用 @abstractmethod 装饰。抽象基类还可以包含具体方法:
from abc import ABC, abstractmethod
class Shape(ABC):
def __init__(self, name):
self.name = name
@abstractmethod
def area(self): # 子类必须实现
pass
def greet(self): # 具体方法,可选覆盖
"""通用逻辑"""
return f"I'm a {self.name}"
class Circle(Shape):
def __init__(self, name, radius):
super().__init__(name)
self.radius = radius
def area(self):
return 3.14 * self.radius ** 2
c = Circle("round", 5)
print(c.area()) # 78.5
print(c.greet()) # I'm a round
抽象基类的典型实践是将公共实现(具体方法)抽取到父类中,同时强制子类实现某些抽象方法。
4.5 虚拟子类:使用 register 方法
一个类可以被注册为某个抽象基类的“虚拟子类”,它并不继承自该 ABC,但 issubclass() 和 isinstance() 仍会返回 True。
class PizzaTopping:
"""普通类,并未继承 Shape 或任何 ABC"""
def area(self):
return "topping area"
def greet(self):
return "I'm a topping"
# 注册 Shape 与 PizzaTopping 的关系
Shape.register(PizzaTopping)
print(issubclass(PizzaTopping, Shape)) # True
p = PizzaTopping()
print(isinstance(p, Shape)) # True
这正是“你希望某个类被当作 ABC 的子类,但由于耦合性不希望它继承”时的常用伎俩。
4.6 __subclasshook__ 方法:按结构判断子类关系
__subclasshook__ 是 ABCMeta 元类中的一个特殊方法,用于控制 issubclass() 的判断逻辑,它允许一个类即使没有显式继承 ABC,也能被视为其子类。
__subclasshook__ 的工作原理
from abc import ABCMeta, abstractmethod
class Sized(metaclass=ABCMeta):
__slots__ = ()
@classmethod
def __subclasshook__(cls, C):
# 如果 C 有 __len__ 方法,就认为是 Sized 的子类
if any("__len__" in B.__dict__ for B in C.__mro__):
return True
return NotImplemented
class SimpleList:
def __len__(self):
return 3
print(issubclass(SimpleList, Sized)) # True
该特殊方法返回 True 时,该类被视为虚拟子类;返回 NotImplemented 时,继续正常的 MRO 查找。
需要注意:标准库在实现 __subclasshook__ 时已足够严谨,通常不需要开发者自行实现。
4.7 ABC 使用总结:四种检查机制
| 运行时 ABC 检查 | isinstance(obj, ABC) | 显式继承了 ABC 或是虚拟子类 |
| __subclasshook__ 增强 | 自定义 __subclasshook__ | 对实现了某类结构但未显式继承的类进行检查 |
| __instancecheck__ 控制 | 自定义元类的 __instancecheck__ | 对 isinstance(obj, Cls) 定义全量子类规则 |
| 静态类型检查 + 协议 | typing.Protocol | IDE 和 mypy 静态检查,无运行时开销 |
五、白鹅类型:抽象基类的实际应用
5.1 白鹅类型的演变
白鹅类型(Goose Typing):指继承自 collections.abc 中的抽象基类(ABC),或者注册为虚拟子类,从而获得完整的集合接口。
从 Python 2.6 开始,由抽象基类支持的方式,会在运行时检查对象是否符合抽象基类的要求。这个概念的核心演变是:
| 鸭子类型 | 1980s | 运行时(错过方法则异常) | 极高 |
| 静态协议 | 2010s | 静态类型检查 | 中 |
| 白鹅类型 | Python 2.6+ | 运行时(isinstance)+ 注册 | 平衡结构性与灵活性 |
5.2 白鹅类型更安全
对比以下两种写法:
# 鸭子类型写法(潜在问题较多)
def f(seq):
try:
# 无约束,可能被传入不支持序列行为的对象
return seq[0] * 2
except TypeError:
return ""
# 白鹅类型写法(结构清晰,可预测)
from collections.abc import Sequence
def f(seq: Sequence) –> float:
"""只接受序列类型"""
return seq[0] * 2
f([1, 2, 3]) # 2
f((4, 5, 6)) # 8
# f({"key": 42}) # mypy 报错,逻辑端却很快就能发现问题
5.3 ABC 在实际项目中的典型用途
六、collections.abc 抽象基类详解
6.1 Collection 抽象基类
Collection 是一个抽象基类,它整合了三个基本抽象基类:Sized、Iterable、Container。
from collections.abc import Collection
def process_collection(c: Collection) –> None:
print(f"size: {len(c)}") # 来自 Sized
for item in c: # 来自 Iterable
print(item)
print("hello" in c) # 来自 Container
process_collection([1, 2, 3])
6.2 Sequence 抽象基类
通过实现 __len__ 和 __getitem__,你就能获得完整的 Sequence 接口。同时,Python 自动提供 __iter__、__contains__、__reversed__、index 和 count 等方法。
class MySequence(Sequence):
def __init__(self, data):
self._data = list(data)
def __len__(self):
return len(self._data)
def __getitem__(self, idx):
return self._data[idx]
seq = MySequence([1, 2, 3])
print(seq.count(2)) # 1,自动获得
print(seq.index(2)) # 1,自动获得
print(list(seq)) # [1, 2, 3]
6.3 ABC 类在框架中的重要作用
collections.abc 中的抽象基类可直接用作类型提示,例如 Sequence、Mapping、Iterator 等让代码意图更明确。
from collections.abc import Sequence
def normalize(v: Sequence[float]) –> float:
"""计算向量模长"""
return sum(x * x for x in v) ** 0.5
七、类型图:四种形式化接口方式
Python 从 3.8 开始,有以下四种定义和使用接口的方式。
#mermaid-svg-AB07g7YvRW2YZTvx{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AB07g7YvRW2YZTvx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AB07g7YvRW2YZTvx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AB07g7YvRW2YZTvx .error-icon{fill:#552222;}#mermaid-svg-AB07g7YvRW2YZTvx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AB07g7YvRW2YZTvx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AB07g7YvRW2YZTvx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AB07g7YvRW2YZTvx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AB07g7YvRW2YZTvx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AB07g7YvRW2YZTvx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AB07g7YvRW2YZTvx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AB07g7YvRW2YZTvx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AB07g7YvRW2YZTvx .marker.cross{stroke:#333333;}#mermaid-svg-AB07g7YvRW2YZTvx svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AB07g7YvRW2YZTvx p{margin:0;}#mermaid-svg-AB07g7YvRW2YZTvx .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-AB07g7YvRW2YZTvx .cluster-label text{fill:#333;}#mermaid-svg-AB07g7YvRW2YZTvx .cluster-label span{color:#333;}#mermaid-svg-AB07g7YvRW2YZTvx .cluster-label span p{background-color:transparent;}#mermaid-svg-AB07g7YvRW2YZTvx .label text,#mermaid-svg-AB07g7YvRW2YZTvx span{fill:#333;color:#333;}#mermaid-svg-AB07g7YvRW2YZTvx .node rect,#mermaid-svg-AB07g7YvRW2YZTvx .node circle,#mermaid-svg-AB07g7YvRW2YZTvx .node ellipse,#mermaid-svg-AB07g7YvRW2YZTvx .node polygon,#mermaid-svg-AB07g7YvRW2YZTvx .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AB07g7YvRW2YZTvx .rough-node .label text,#mermaid-svg-AB07g7YvRW2YZTvx .node .label text,#mermaid-svg-AB07g7YvRW2YZTvx .image-shape .label,#mermaid-svg-AB07g7YvRW2YZTvx .icon-shape .label{text-anchor:middle;}#mermaid-svg-AB07g7YvRW2YZTvx .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AB07g7YvRW2YZTvx .rough-node .label,#mermaid-svg-AB07g7YvRW2YZTvx .node .label,#mermaid-svg-AB07g7YvRW2YZTvx .image-shape .label,#mermaid-svg-AB07g7YvRW2YZTvx .icon-shape .label{text-align:center;}#mermaid-svg-AB07g7YvRW2YZTvx .node.clickable{cursor:pointer;}#mermaid-svg-AB07g7YvRW2YZTvx .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AB07g7YvRW2YZTvx .arrowheadPath{fill:#333333;}#mermaid-svg-AB07g7YvRW2YZTvx .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AB07g7YvRW2YZTvx .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AB07g7YvRW2YZTvx .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AB07g7YvRW2YZTvx .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AB07g7YvRW2YZTvx .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AB07g7YvRW2YZTvx .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AB07g7YvRW2YZTvx .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AB07g7YvRW2YZTvx .cluster text{fill:#333;}#mermaid-svg-AB07g7YvRW2YZTvx .cluster span{color:#333;}#mermaid-svg-AB07g7YvRW2YZTvx div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AB07g7YvRW2YZTvx .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AB07g7YvRW2YZTvx rect.text{fill:none;stroke-width:0;}#mermaid-svg-AB07g7YvRW2YZTvx .icon-shape,#mermaid-svg-AB07g7YvRW2YZTvx .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AB07g7YvRW2YZTvx .icon-shape p,#mermaid-svg-AB07g7YvRW2YZTvx .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AB07g7YvRW2YZTvx .icon-shape .label rect,#mermaid-svg-AB07g7YvRW2YZTvx .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AB07g7YvRW2YZTvx .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AB07g7YvRW2YZTvx .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AB07g7YvRW2YZTvx :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
Python 中的类型化体系
鸭子类型动态协议Duck Typing
白鹅类型抽象基类Goose Typing
静态类型Static Typing
静态协议Static Protocol
运行时检查属性缺失报错
运行时检查isinstance/issubclass
编译/静态时检查mypy/Pyright
编译/静态时检查mypy + Protocol
本章重点分析白鹅类型(Goose Typing)的机制,以及开发者如何通过 ABC 实现运行时结构检查。
八、设计示例:电商折扣系统
下面以一个自定义抽象基类和虚拟子类的完整案例来收尾,展示抽象基类在实际开发中的典型使用场景。
from abc import ABC, abstractmethod
from decimal import Decimal
from typing import NamedTuple
from collections.abc import Sequence
class Customer(NamedTuple):
name: str
fidelity: int
class LineItem(NamedTuple):
product: str
quantity: int
price: Decimal
def total(self) –> Decimal:
return self.price * self.quantity
class Order(NamedTuple):
customer: Customer
cart: Sequence[LineItem]
promotion: 'Promotion | None' = None
def total(self) –> Decimal:
return sum(item.total() for item in self.cart, start=Decimal(0))
def due(self) –> Decimal:
discount = self.promotion.discount(self) if self.promotion else Decimal(0)
return self.total() – discount
def __repr__(self):
return f'<Order total: {self.total():.2f} due: {self.due():.2f}>'
# 定义一个抽象基类(= 正式接口)
class Promotion(ABC):
@abstractmethod
def discount(self, order: Order) –> Decimal:
"""返回折扣金额(Decimal 类型)"""
class FidelityPromo(Promotion):
def discount(self, order: Order) –> Decimal:
if order.customer.fidelity >= 1000:
return order.total() * Decimal('0.05')
return Decimal(0)
class BulkItemPromo(Promotion):
def discount(self, order: Order) –> Decimal:
discount = Decimal(0)
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * Decimal('0.1')
return discount
class LargeOrderPromo(Promotion):
def discount(self, order: Order) –> Decimal:
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * Decimal('0.07')
return Decimal(0)
# 虚拟子类:注册适配器模式的类
class GeneralDiscount:
"""不在原继承体系中的类"""
def calculate(self, order: Order) –> Decimal:
return order.total() * Decimal('0.03')
Promotion.register(GeneralDiscount)
g = GeneralDiscount()
print(isinstance(g, Promotion)) # True,虚拟代理
# 调用示例
ann = Customer('Ann', 1200)
items = [LineItem('phone', 1, Decimal('699'))]
order = Order(ann, items, FidelityPromo())
print(order.due()) # 抵扣 5% 后 = 664.05
这个示例的完整度较高,涵盖了抽象基类的创建、具体子类的实现和虚拟子类的注册。抽象基类 Promo 作为正式接口,要求所有策略必须实现 discount 方法,这种契约既能在运行时检查,又能提供清晰的文档。
接口与抽象基类体系架构
#mermaid-svg-kBFDVBlaKImY1i7g{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kBFDVBlaKImY1i7g .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kBFDVBlaKImY1i7g .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kBFDVBlaKImY1i7g .error-icon{fill:#552222;}#mermaid-svg-kBFDVBlaKImY1i7g .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kBFDVBlaKImY1i7g .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kBFDVBlaKImY1i7g .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kBFDVBlaKImY1i7g .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kBFDVBlaKImY1i7g .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kBFDVBlaKImY1i7g .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kBFDVBlaKImY1i7g .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kBFDVBlaKImY1i7g .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kBFDVBlaKImY1i7g .marker.cross{stroke:#333333;}#mermaid-svg-kBFDVBlaKImY1i7g svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kBFDVBlaKImY1i7g p{margin:0;}#mermaid-svg-kBFDVBlaKImY1i7g g.classGroup text{fill:#9370DB;stroke:none;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-kBFDVBlaKImY1i7g g.classGroup text .title{font-weight:bolder;}#mermaid-svg-kBFDVBlaKImY1i7g .cluster-label text{fill:#333;}#mermaid-svg-kBFDVBlaKImY1i7g .cluster-label span{color:#333;}#mermaid-svg-kBFDVBlaKImY1i7g .cluster-label span p{background-color:transparent;}#mermaid-svg-kBFDVBlaKImY1i7g .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-kBFDVBlaKImY1i7g .cluster text{fill:#333;}#mermaid-svg-kBFDVBlaKImY1i7g .cluster span{color:#333;}#mermaid-svg-kBFDVBlaKImY1i7g .nodeLabel,#mermaid-svg-kBFDVBlaKImY1i7g .edgeLabel{color:#131300;}#mermaid-svg-kBFDVBlaKImY1i7g .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-kBFDVBlaKImY1i7g .label text{fill:#131300;}#mermaid-svg-kBFDVBlaKImY1i7g .labelBkg{background:#ECECFF;}#mermaid-svg-kBFDVBlaKImY1i7g .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-kBFDVBlaKImY1i7g .classTitle{font-weight:bolder;}#mermaid-svg-kBFDVBlaKImY1i7g .node rect,#mermaid-svg-kBFDVBlaKImY1i7g .node circle,#mermaid-svg-kBFDVBlaKImY1i7g .node ellipse,#mermaid-svg-kBFDVBlaKImY1i7g .node polygon,#mermaid-svg-kBFDVBlaKImY1i7g .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kBFDVBlaKImY1i7g .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-kBFDVBlaKImY1i7g g.clickable{cursor:pointer;}#mermaid-svg-kBFDVBlaKImY1i7g g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-kBFDVBlaKImY1i7g g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-kBFDVBlaKImY1i7g .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-kBFDVBlaKImY1i7g .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-kBFDVBlaKImY1i7g .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-kBFDVBlaKImY1i7g .dashed-line{stroke-dasharray:3;}#mermaid-svg-kBFDVBlaKImY1i7g .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-kBFDVBlaKImY1i7g #compositionStart,#mermaid-svg-kBFDVBlaKImY1i7g .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-kBFDVBlaKImY1i7g #compositionEnd,#mermaid-svg-kBFDVBlaKImY1i7g .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-kBFDVBlaKImY1i7g #dependencyStart,#mermaid-svg-kBFDVBlaKImY1i7g .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-kBFDVBlaKImY1i7g #dependencyStart,#mermaid-svg-kBFDVBlaKImY1i7g .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-kBFDVBlaKImY1i7g #extensionStart,#mermaid-svg-kBFDVBlaKImY1i7g .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-kBFDVBlaKImY1i7g #extensionEnd,#mermaid-svg-kBFDVBlaKImY1i7g .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-kBFDVBlaKImY1i7g #aggregationStart,#mermaid-svg-kBFDVBlaKImY1i7g .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-kBFDVBlaKImY1i7g #aggregationEnd,#mermaid-svg-kBFDVBlaKImY1i7g .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-kBFDVBlaKImY1i7g #lollipopStart,#mermaid-svg-kBFDVBlaKImY1i7g .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-kBFDVBlaKImY1i7g #lollipopEnd,#mermaid-svg-kBFDVBlaKImY1i7g .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-kBFDVBlaKImY1i7g .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-kBFDVBlaKImY1i7g .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kBFDVBlaKImY1i7g .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-kBFDVBlaKImY1i7g .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-kBFDVBlaKImY1i7g :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
register virtual subclass
«ABC»
Promotion
+discount(order) : Decimal
FidelityPromo
+discount(order) : Decimal
BulkItemPromo
+discount(order) : Decimal
LargeOrderPromo
+discount(order) : Decimal
GeneralDiscount
+calculate(order) : Decimal
Order
-customer: Customer
-cart: list[LineItem]
-promotion: Promotion?
+total() : Decimal
+due() : Decimal
+repr()
九、本章思维导图
#mermaid-svg-7m1EIPcSLxviTsP8{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7m1EIPcSLxviTsP8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7m1EIPcSLxviTsP8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7m1EIPcSLxviTsP8 .error-icon{fill:#552222;}#mermaid-svg-7m1EIPcSLxviTsP8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7m1EIPcSLxviTsP8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7m1EIPcSLxviTsP8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7m1EIPcSLxviTsP8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7m1EIPcSLxviTsP8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7m1EIPcSLxviTsP8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7m1EIPcSLxviTsP8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7m1EIPcSLxviTsP8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7m1EIPcSLxviTsP8 .marker.cross{stroke:#333333;}#mermaid-svg-7m1EIPcSLxviTsP8 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7m1EIPcSLxviTsP8 p{margin:0;}#mermaid-svg-7m1EIPcSLxviTsP8 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-7m1EIPcSLxviTsP8 .cluster-label text{fill:#333;}#mermaid-svg-7m1EIPcSLxviTsP8 .cluster-label span{color:#333;}#mermaid-svg-7m1EIPcSLxviTsP8 .cluster-label span p{background-color:transparent;}#mermaid-svg-7m1EIPcSLxviTsP8 .label text,#mermaid-svg-7m1EIPcSLxviTsP8 span{fill:#333;color:#333;}#mermaid-svg-7m1EIPcSLxviTsP8 .node rect,#mermaid-svg-7m1EIPcSLxviTsP8 .node circle,#mermaid-svg-7m1EIPcSLxviTsP8 .node ellipse,#mermaid-svg-7m1EIPcSLxviTsP8 .node polygon,#mermaid-svg-7m1EIPcSLxviTsP8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7m1EIPcSLxviTsP8 .rough-node .label text,#mermaid-svg-7m1EIPcSLxviTsP8 .node .label text,#mermaid-svg-7m1EIPcSLxviTsP8 .image-shape .label,#mermaid-svg-7m1EIPcSLxviTsP8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-7m1EIPcSLxviTsP8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7m1EIPcSLxviTsP8 .rough-node .label,#mermaid-svg-7m1EIPcSLxviTsP8 .node .label,#mermaid-svg-7m1EIPcSLxviTsP8 .image-shape .label,#mermaid-svg-7m1EIPcSLxviTsP8 .icon-shape .label{text-align:center;}#mermaid-svg-7m1EIPcSLxviTsP8 .node.clickable{cursor:pointer;}#mermaid-svg-7m1EIPcSLxviTsP8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7m1EIPcSLxviTsP8 .arrowheadPath{fill:#333333;}#mermaid-svg-7m1EIPcSLxviTsP8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7m1EIPcSLxviTsP8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7m1EIPcSLxviTsP8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7m1EIPcSLxviTsP8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7m1EIPcSLxviTsP8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7m1EIPcSLxviTsP8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7m1EIPcSLxviTsP8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7m1EIPcSLxviTsP8 .cluster text{fill:#333;}#mermaid-svg-7m1EIPcSLxviTsP8 .cluster span{color:#333;}#mermaid-svg-7m1EIPcSLxviTsP8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7m1EIPcSLxviTsP8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7m1EIPcSLxviTsP8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-7m1EIPcSLxviTsP8 .icon-shape,#mermaid-svg-7m1EIPcSLxviTsP8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7m1EIPcSLxviTsP8 .icon-shape p,#mermaid-svg-7m1EIPcSLxviTsP8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7m1EIPcSLxviTsP8 .icon-shape .label rect,#mermaid-svg-7m1EIPcSLxviTsP8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7m1EIPcSLxviTsP8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7m1EIPcSLxviTsP8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7m1EIPcSLxviTsP8 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
工作原理
issubclass(C, ABC) 调用
检查 C 是否具备指定方法
返回 True / NotImplemented
第13章 接口:从协议到抽象基类
动态协议
序列协议
鸭子类型 Duck Typing
抽象基类
定义ABC
@abstractmethod
具体方法也允许
使用 ABC
isinstance()/issubclass()
虚拟子类注册 register
collections.abc
Sequence
Mapping
Set
Collection
白鹅类型 Goose Typing
从鸭子类型到白鹅类型
运行时接口检查
框架设计最佳实践
__subclasshook__ 机制
四种类型化方式的对比
十、常见错误与最佳实践
| 忘记实现抽象方法 | 子类没有实现所有 @abstractmethod | 实例化时会引发 TypeError |
| 错误使用 NotImplemented 与 NotImplementedError | 两者概念混淆 | NotImplemented 是返回值,NotImplementedError 是异常 |
| 滥用 __subclasshook__ | 引入太宽松的虚拟继承关系 | 尽量使用显式注册或正常的继承关系 |
| 在核心逻辑中使用 isinstance(obj, ABC) 判断 | 窄化了应支持的接口范围 | 依赖于动态协议或类型提示更符合鸭式编程 |
| 对简单项目过度引入 ABC | 不匹配的问题复杂度 | 根据项目规模决定是否使用 ABC |
最佳实践总结:
- 优先使用鸭子类型:对于小型模块或内部逻辑,灵活的动态协议更好用。
- 在框架/接口设计中选用 ABC:企业级多人协作时常需要用抽象基类统一约定。
- 认识到 ABC 的价值不在强制而在于文档:抽象基类明确声明了“本对象应该支持的行为”。
- 注册虚拟子类的最佳场景:处理旧代码或第三方库,无法直接让其继承。
- 将 __subclasshook__ 视为“高级特性”:除非万不得已,避免重写。
十一、本章总结
第 13 章“接口:从协议到抽象基类”学到这里,可以看到 Python 为接口设计提供了从“纯动态”到“纯静态”的完整光谱:
动态协议(Duck Typing) :只需实现所需方法,无需显式继承。这是 Python 早期版本的默认模式,也是编程思维演变的出发地。
正式接口(ABC) :collections.abc 提供了丰富的可直接复用的抽象基类;同时你也可以通过 abc 模块定义自己的抽象基类,可以自定义虚拟子类。
虚拟子类与 __subclasshook__ :在保持 ABC 文档能力的同时,实现对第三方类的运行时类型识别。
白鹅类型(Goose Typing) :ABC 在 Python 工业界实践中的名称,平衡了动态灵活性和大型系统约束性的需求。
将这几节内容贯穿起来,你就能把握住 Python 接口设计的演进脉络:从完全的“运行期容错”到运行时“结构性检查”,再转到静态的“编译时契约”。在当今 Python 类型提示生态日益完善的背景下,大型 Python 项目多采用这种方式组织代码。
十二、思考题
协议 vs 抽象基类:鸭子类型(动态协议)与抽象基类的主要区别是什么?二者分别适合何种场景?
__subclasshook__ 与虚拟子类:思考可否不用 __subclasshook__,仅通过 register 方法实现对第三方类的接口识别?两者各自解决哪类问题?
白鹅类型与类型提示:白鹅类型在运行时的 isinstance(obj, ABC) 检查,与静态类型检查工具(如 mypy)中的 Protocol 有哪些本质的区别?
设计决策:假设你正在设计一个事件框架,要求所有事件处理器必须实现 handle(event: Event) 方法。你会选择用抽象基类(强制 @abstractmethod)还是根据鸭子类型约定在文档中给出建议?说明理由。
继承 vs 虚拟子类:为什么 Python 的抽象基类要支持虚拟子类注册?这样做解决了哪些设计问题,又带来了哪些副作用?
Sequence ABC 需求:只实现 __len__ 和 __getitem__,就能得到 Sequence 自动提供的 __contains__、__iter__、count、index 等方法。追溯 collections.abc.Sequence 的源码,看它内部包含的具体方法。
十三、下一章预告
第 14 章《继承:优缺点》
学完“接口与抽象基类”这一章之后,我们获得了更好的正式接口协议。下一章将结合 Abstract Base Class 进一步深入到经典世界:多重继承。第 14 章的内容包括:
- 方法解析顺序(MRO) :Python 使用 C3 线性化算法处理继承层次中的方法搜索。
- 混入类(Mixin)设计模式:使用多重继承的最佳实践。
- 实用性建议:继承是个好工具吗?什么时候用组合优于继承?
- 避免钻石继承问题:通过具体的例子明了多重继承的常见陷阱。
无论你是否曾纠结于多重继承,第 14 章都会为你提供清晰的解答。
本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!



