typing.Protocol 是仅用于静态类型检查的结构化接口,不支持运行时实例检查或强制实现;而 ABC 是运行时抽象基类,支持 isinstance 和 @abstractmethod。

typing.Protocol 是什么,它和 ABC 有什么本质区别?
typing.Protocol 不是运行时的抽象基类(ABC),它只在类型检查阶段起作用——mypy、pyright、PyCharm 的类型系统能识别它,但 isinstance(obj, MyProtocol) 会报 TypeError: Instance checking not supported for protocol classes。这意味着你不能用它做运行时类型分发,也不能靠它强制子类实现方法。它的核心价值是“只要结构匹配,就视为兼容”,也就是鸭子类型在类型系统里的正式表达。
常见错误是试图继承 Protocol 并调用 super().__init__(),或给协议加 @abstractmethod——Protocol 本身不支持这些。它只关心“有没有这个属性/方法签名”,不关心“怎么实现”。
如何正确定义一个 Protocol 并让类“隐式满足”它?
定义协议时只需声明方法签名和属性类型,不用 pass 或实现体;类只要公开拥有完全匹配的成员(名称 + 类型注解),就会被类型检查器自动视为该协议的实现者——无需显式继承或注册。
例如:
from typing import Protocol
<p>class Drawable(Protocol):
def draw(self) -> None: ...
color: str</p><p>class Circle:
def <strong>init</strong>(self, color: str) -> None:
self.color = color
def draw(self) -> None:
print(f"Drawing {self.color} circle")</p><p>def render(d: Drawable) -> None:
d.draw()</p><p>render(Circle("red")) # ✅ 类型检查通过</p>
注意点:
-
...是协议方法的合法占位符,不是省略号语法;写pass也行,但...更符合惯例 - 属性必须带类型注解(如
color: str),仅定义self.color = ...不足以让类型检查器推断出协议属性 - 参数名必须一致:如果协议写
def draw(self, scale: float) -> None,而类里是def draw(self, s: float),mypy 会报错
什么时候该用 Protocol 而不是继承 ABC 或普通基类?
当你需要跨模块、跨继承体系复用接口契约,且不希望引入强耦合时,Protocol 是更轻量的选择。比如标准库的 Iterable、Mapping 就是 Protocol,所以 list、dict、自定义生成器都能被识别为 Iterable[T],哪怕它们没继承任何共同父类。
典型适用场景:
- 为第三方类(如
requests.Response)建模行为,又不想 monkey patch 或包装 - 函数参数需接受多种不相关类型,只要它们都有
.json()和.status_code(类似requests.Response、httpx.Response、mock 对象) - 避免因继承顺序导致的菱形问题,尤其在已有类层次较深时
反例:如果你需要运行时检查(isinstance(x, MyInterface))、强制子类实现、或依赖 __subclasshook__,那就该用 abc.ABC + @abstractmethod。
Protocol 的常见陷阱和性能影响
协议本身没有运行时开销——它被完全擦除,不会出现在 __annotations__ 或生成字节码中。但容易踩的坑不少:
- 协变/逆变没声明:默认所有泛型参数是不变的。如果协议含
def get_item(self) -> T,而你传入List[int]给期望Container[object]的函数,会报错;此时需用typing.TypeVar("T", covariant=True) - 可选成员难表达:协议里不能直接写
attr: Optional[str] = None。正确方式是拆成两个协议,或用typing.runtime_checkable+ 运行时逻辑兜底 - 嵌套协议易漏注解:比如协议中返回另一个协议实例,必须显式标注返回类型为该协议,否则类型检查器无法向下传导
最常被忽略的是:协议一旦用于泛型约束(如 def f[T: MyProtocol](x: T)),就要求所有满足类型的 __init__ 签名也兼容——这点在构造函数参数多变时特别容易爆红,得退回到非泛型或用 cast 临时绕过。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











