Python抽象基类vs协议对比
Python 抽象基类 vs 协议对比Python 提供两种接口定义方式ABC抽象基类显式声明继承关系Protocol协议支持结构子类型duck typing。本文深入对比两者。1. ABC——显式接口is-a 关系-----------------------------------ABC 使用 abstractmethod 定义接口子类必须显式继承。from abc import ABC, abstractmethodclass PaymentProcessor(ABC):支付处理器抽象基类——定义接口契约abstractmethoddef pay(self, amount: float) - bool:执行支付返回是否成功...abstractmethoddef refund(self, transaction_id: str) - bool:执行退款...def get_fee(self, amount: float) - float:非抽象方法——提供默认实现return amount * 0.01 # 1% 手续费class AlipayProcessor(PaymentProcessor):支付宝支付——必须实现所有抽象方法def pay(self, amount: float) - bool:print(f支付宝支付: {amount})return Truedef refund(self, transaction_id: str) - bool:print(f支付宝退款: {transaction_id})return True# get_fee 使用默认实现class WechatPayProcessor(PaymentProcessor):微信支付——也必须实现所有抽象方法def pay(self, amount: float) - bool:print(f微信支付: {amount})return Truedef refund(self, transaction_id: str) - bool:print(f微信退款: {transaction_id})return True# 类型检查——isinstance 可以验证proc AlipayProcessor()print(isinstance(proc, PaymentProcessor)) # True# 未实现抽象方法时实例化会报错class IncompleteProcessor(PaymentProcessor):pass# ip IncompleteProcessor() # TypeError!2. Protocol——结构子类型duck typing------------------------------------------Protocol 只要对象有相应方法/属性即视为兼容无需显式继承。from typing import Protocolclass Flyable(Protocol):飞行能力协议——有 fly 方法的都是 Flyabledef fly(self) - str: ...class Bird:鸟——有 fly 方法自动满足 Flyable 协议def fly(self) - str:return 鸟儿飞翔class Airplane:飞机——也有 fly 方法同样满足 Flyable 协议def fly(self) - str:return 飞机飞行class Fish:鱼——没有 fly 方法不满足 Flyable 协议def swim(self) - str:return 鱼儿游泳def let_it_fly(thing: Flyable) - None:接受任何 Flyable 对象结构子类型print(thing.fly())# 无需继承 Flyable——自动满足let_it_fly(Bird()) # 鸟儿飞翔let_it_fly(Airplane()) # 飞机飞行# let_it_fly(Fish()) # 类型检查会报错静态3. runtime_checkable——运行时检查协议------------------------------------------默认 Protocol 不支持 isinstance需加装饰器。from typing import Protocol, runtime_checkableruntime_checkableclass Drawable(Protocol):可绘制协议——支持运行时 isinstance 检查def draw(self) - str: ...class Circle:def draw(self) - str:return 绘制圆形class Square:def draw(self) - str:return 绘制正方形# 现在 isinstance 可工作c Circle()print(isinstance(c, Drawable)) # True——有 draw 方法# 注意runtime_checkable 只检查方法名是否存在不检查签名class WrongDraw:def draw(self, extra_arg: str) - str:return 参数签名不匹配但也能通过print(isinstance(WrongDraw(), Drawable)) # True——只检查方法存在4. ABC.register()——注册虚拟子类---------------------------------------无需修改源代码让一个类成为 ABC 的虚拟子类。from abc import ABCclass MyIterable(ABC):自定义可迭代抽象基类abstractmethoddef __iter__(self):...classmethoddef __subclasshook__(cls, other):只要实现了 __iter__ 就算子类if cls is MyIterable:if any(__iter__ in B.__dict__ for B in other.__mro__):return Truereturn NotImplemented# 注册第三方类为虚拟子类class ExternalList:外部列表类——无法修改源代码def __iter__(self):return iter([1, 2, 3])# 方式一registerMyIterable.register(ExternalList)print(isinstance(ExternalList(), MyIterable)) # True# 方式二__subclasshook__如上所示自动检测结构匹配5. __subclasshook__——自定义子类检测------------------------------------------在 ABC 中定义 __subclasshook__ 来检测结构匹配。from abc import ABC, abstractmethodclass Hashable(ABC):可哈希接口——使用 __subclasshook__ 自动检测abstractmethoddef __hash__(self) - int:...classmethoddef __subclasshook__(cls, other):if cls is Hashable:# 检查是否实现了 __hash__for B in other.__mro__:if __hash__ in B.__dict__:return Truereturn NotImplemented# Python 内置的 str 自动匹配print(isinstance(hello, Hashable)) # True——str 有 __hash__print(isinstance([1, 2], Hashable)) # False——list 没有 __hash__6. collections.abc 使用模式-------------------------------标准库提供了丰富的 ABC应优先使用。from collections.abc import Sequence, MutableMapping, Iterableclass MyList(Sequence):实现 Sequence 接口——自动获得 __contains__、__iter__ 等def __init__(self, items):self._items list(items)def __getitem__(self, index):return self._items[index]def __len__(self):return len(self._items)# 只需要实现 __getitem__ 和 __len__# Sequence 自动提供__contains__、__iter__、reversed、index、countml MyList([1, 2, 3, 4, 5])print(3 in ml) # True——来自 Sequencefor x in ml:print(x, end ) # 1 2 3 4 5——来自 Sequenceprint()print(ml.index(3)) # 2——来自 Sequence7. 如何选择ABC vs Protocol--------------------------------选择 ABC- 明确的 is-a 关系如 支付宝是一种支付处理器- 需要提供默认方法实现- 需要 __subclasshook__ 或 register()- 框架要求继承特定基类如 Django、SQLAlchemy选择 Protocol- 关注能做什么而非是什么duck typing- 不希望强制继承关系- 需要与第三方库代码协作且不能修改源码- 希望保持类型检查同时保持 Python 的灵活性经验法则当你想定义一个东西能做某事时用 Protocol当你想定义一个东西是什么时用 ABC。# 典型 Protocol 场景鸭子类型class Printable(Protocol):def __str__(self) - str: ...def display(item: Printable) - None:接受任何可打印的对象print(str(item))# 典型 ABC 场景家族层次class Vehicle(ABC):abstractmethoddef start(self) - None: ...abstractmethoddef stop(self) - None: ...def honk(self) - None:print(喇叭鸣响) # 默认实现class Car(Vehicle):def start(self) - None:print(汽车启动)def stop(self) - None:print(汽车停止)总结ABC 适用于明确的继承层次和需要默认实现的场景Protocol 适用于鸭子类型和松耦合场景。两者并非对立——ABC 侧重是什么显式接口Protocol 侧重能做什么结构子类型。现代 Python 倾向于在类型提示中使用 Protocol 以获得更大灵活性。