跳转至

Protocol 和 ABC 在定义接口时有什么本质区别?

🔷 Protocol 和 ABC 在定义接口时的本质区别?

🎯 先给结论

ABC 看的是“你继承了我吗?”(名义子类型),Protocol 看的是“你能做这些事吗?”(结构子类型)。

换句话说,ABC 讲血缘,Protocol 讲能力。


🔍 1. ABC——抽象基类:必须“签合同”

abc.ABC@abstractmethod 定义接口,子类必须显式继承,否则运行时根本不认。

from abc import ABC, abstractmethod

class Flyable(ABC):
    @abstractmethod
    def fly(self) -> None:
        ...

class Bird(Flyable):       # 显式继承
    def fly(self) -> None:
        print("扑翼飞行")

# 运行时检查立刻生效
print(isinstance(Bird(), Flyable))  # True

📌 特点:

  • 强侵入性,想用接口就得改类定义或显式 register

  • 运行时 isinstance / issubclass 会严格检查继承链

  • 典型的“框架基类”思维:你在我的体系里,就得按我的规则来


🔷 2. Protocol——协议:看你会不会“做事”

typing.Protocol 定义了结构接口,任何类只要拥有了规定的方法签名,无需继承,就自动被视为该类型。这本质是静态鸭子类型。

from typing import Protocol

class Flyable(Protocol):
    def fly(self) -> None:
        ...

class Bird:                # 不继承任何东西
    def fly(self) -> None:
        print("扑翼飞行")

class Airplane:
    def fly(self) -> None:
        print("喷气飞行")

def take_off(f: Flyable) -> None:
    f.fly()

take_off(Bird())      # ✅ 静态检查通过
take_off(Airplane())  # ✅ 静态检查通过

⚠️ 注意:上面这种“类型兼容”默认只在静态检查器(mypy、pyright)里生效,运行时 isinstance(Bird(), Flyable) 会直接报错,除非用 @runtime_checkable 装饰器显式开启运行时支持。

深入一点:为什么会有这种区别?

ABC 的本质是运行时强制契约。当我在一个框架里写出 isinstance(obj, MyBase) 时,我希望得到绝对确定的答案,所以必须依靠继承关系。

Protocol 的本质是静态类型系统里的鸭子类型。Python 一直崇尚“如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子”。Protocol 就是把这种哲学搬进了类型标注里,让静态检查器也能懂“你只要有 fly() 方法,就是 Flyable”。

这就意味着:

  • Protocol 更 Pythonic,允许你写出不依赖具体类型的泛型代码,且不强迫第三方库继承你的基类。

  • ABC 更“企业级安全”,当你确实需要运行时类型保障(比如动态加载插件),ABC 提供硬性约束。

面试时怎么收尾?

你可以这样自然地说:

“选 Protocol 还是 ABC,本质是在选‘静态结构检查’还是‘运行时契约’。 如果我只是想约束函数参数的行为,用 Protocol 最轻量,不侵入代码,静态检查就能覆盖大部分错误。 但如果我写的是一个框架,需要在运行时根据类型做分支判断,或者我想强制子类实现某个方法,那 ABC 就更合适。 当然,两个也不是完全对立,@runtime_checkable 的 Protocol 就是两者的一种折中——既有结构判断,又能在运行时用 isinstance。理解它们背后的思想,比背答案重要得多。”