欢迎光临
我们一直在努力

《流畅的Python》读书笔记14: 第三部分 类和协议 - 从协议到抽象基类

作者: 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 核心作用

  • 提供标准接口定义:collections.abc 中的抽象基类为序列、集合、映射等提供了标准接口,使接口定义在多处保持一致。
  • 运行时类型检查:通过 isinstance() 和 issubclass() 判断对象是否实现了某个接口。
  • 虚拟子类机制:类可以不通过继承便被视为抽象基类的子类,这在框架设计和旧代码适配时特别有用。
  • 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 在实际项目中的典型用途

  • 框架内的插件架构:要求所有插件必须实现特定的抽象方法。
  • 统一运行时接口检查:配合 isinstance(obj, abc) 在大型项目中强制契约。
  • 文档和 IDE 辅助:抽象基类使代码意图表达更清晰。
  • 类型提示:许多类型提示(Sequence、Mapping)源于 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 章都会为你提供清晰的解答。


    本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
    👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!

    赞(0)
    未经允许不得转载:171主机测评 » 《流畅的Python》读书笔记14: 第三部分 类和协议 - 从协议到抽象基类
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址