跳转至

什么时候用 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 根本不能写方法体。

  • 例子:MessageSenderbatch_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 强调的是‘你只要会做这些事就行’——选择哪种,取决于你想要的是继承树带来的共享与强制,还是结构匹配带来的灵活与解耦。”