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。理解它们背后的思想,比背答案重要得多。”