什么时候用 Protocol,什么时候用 ABC?
在 Python 接口设计中,什么时候应该使用 Protocol,什么时候该用传统的 ABC(抽象基类)?
这本质上是在问:“名义子类型”(Nominal) vs “结构化子类型”(Structural),你到底要哪一种约束?
📦 快速看清两者的定位¶
| 特性 | ABC(abc.ABC) | Protocol(typing.Protocol) |
|---|---|---|
| 📜 核心哲学 | 继承关系——“你是我的子类,就必须实现这些方法” | 结构匹配——“只要你长得像鸭子,你就是鸭子” |
| ⏱️ 检查时机 | 运行时:实例化时若未实现抽象方法会报 TypeError | 静态检查:由 mypy/pyright 在代码分析阶段报告错误 |
| 🔗 是否需要继承 | ✅ 必须显式继承基类 | ❌ 不需要继承,只要对象拥有同名方法即可 |
| 🧬 代码复用 | ✅ 可写具体方法,共享状态和逻辑 | ❌ 只能定义方法签名,不能有实现 |
| 🔍 isinstance 支持 | ✅ 原生支持,且可靠 | 🟡 需加 @runtime_checkable 装饰,且只检查方法存在性 |
| 🧩 典型场景 | 框架内部强约束、模板方法、插件基类 | 跨库类型标注、对第三方类加行为契约、松散耦合 |
🧪 场景化对比:一个“消息发送器”接口¶
🅰️ 使用 ABC:运行时强硬约束¶
from abc import ABC, abstractmethod
class MessageSender(ABC):
@abstractmethod
def send(self, content: str) -> bool:
"""发送消息,必须实现"""
...
def batch_send(self, contents: list[str]) -> int:
# 共享的具体方法,子类可以直接用
return sum(self.send(c) for c in contents)
class EmailSender(MessageSender):
def send(self, content):
print(f"📧 发送邮件: {content}")
return True
# ✅ EmailSender 必须继承 MessageSender,否则实例化就报错
sender = EmailSender()
sender.batch_send(["hello", "world"])
适用场景:
你需要一个家族式的类体系,基类提供公共逻辑(如 batch_send),并且强制所有子类遵循同一模板。框架内部开发、插件系统常用这种方式。
🅱️ 使用 Protocol:静态鸭子类型¶
from typing import Protocol
class Sendable(Protocol):
def send(self, content: str) -> bool: ...
def notify(sender: Sendable, msg: str):
if sender.send(msg):
print("✅ 通知成功")
# 第三方/旧的类,完全不继承 Sendable
class SlackNotifier:
def send(self, content: str) -> bool:
print(f"💬 发送Slack: {content}")
return True
# 静态检查通过,运行也正常
notify(SlackNotifier(), "部署完成")
适用场景:
你要为一个功能定义“需要什么方法”,但不想强迫所有实现者去继承你的基类——尤其是当这些类来自第三方库、或者分散在不同模块时。Protocol 让类型注解与鸭子类型完美结合。
🤔 到底怎么选?问自己两个问题¶
1️⃣ 需要共享代码或公共状态吗?¶
-
如果需要基类提供工具方法、属性或默认实现,必须选 ABC。Protocol 根本不能写方法体。
-
例子:
MessageSender的batch_send复用了send,这种逻辑只能放在 ABC 里。
2️⃣ 需要运行时保障还是静态提醒?¶
-
如果你希望写错代码时 IDE 就能立刻划红线,并且允许灵活的结构匹配,用 Protocol。
-
如果你希望在实例化那一刻就强制拦截未实现的类(确保运行时安全),用 ABC。
-
举个例子:动态加载插件,你无法控制插件类何时被导入,用 ABC 可以在创建实例时报错;用 Protocol 则需要结合
isinstance检查(加上@runtime_checkable),但那只检查方法是否存在,不检查签名。
🧰 我通常这样划分¶
| 状况 | 推荐 |
|---|---|
| 设计一个紧密协作的内部模块体系,要共享代码 | ABC |
| 为整个项目/库定义公共 API,希望其他模块甚至外部使用者都能“适应” | Protocol |
| 需要写 isinstance(obj, Base) 来分派逻辑 | ABC |
| 仅仅为函数参数做类型注解,表示“传进来的对象得有这些方法” | Protocol |
| 第三方类不能改动,但想让它符合我的接口 | Protocol(最闪亮的使用场景) |
| 希望子类不能忘记实现某个方法,否则连运行都跑不起来 | ABC |
💡 进阶视角:Python 类型系统的演进¶
-
ABC 是经典的“面向对象继承”,在 Python 2.6 时代引入,解决接口定义和运行时检查。
-
Protocol 是 PEP 544 引入的“结构化子类型”,让 Python 的静态类型系统不再只能基于继承,也能像 Go 的 interface 或 TypeScript 的 interface 那样,基于结构匹配。
-
实际项目中,两者可以共存:比如一个 ABC 可以作为某些类的基类,同时定义一个同名的 Protocol,让那些不继承 ABC 的类也能通过静态检查。但这属于进阶玩法。
✅ 一句话记住¶
“ABC 强调的是‘你必须是我家族的成员’,Protocol 强调的是‘你只要会做这些事就行’——选择哪种,取决于你想要的是继承树带来的共享与强制,还是结构匹配带来的灵活与解耦。”