typing.Protocol 是 Python 唯一原生支持结构子类型的机制,仅在类型检查时生效,运行时不校验;它通过方法签名匹配实现“只看有没有”,不依赖继承,需用 ... 定义方法体并显式标注类型。

直接说结论:typing.Protocol 是 Python 唯一原生支持结构子类型的机制,但它只在类型检查阶段生效(如 mypy),运行时不校验、不强制、不反射 —— 你写错协议方法名或漏实现,程序照常跑,只有类型检查器会报错。
Protocol 的核心行为:只看“有没有”,不看“是谁”
Python 默认是名义子类型(靠 isinstance 和继承链判断),而 Protocol 让你能基于“具备哪些方法/属性”来建模兼容性。比如一个类没继承 Readable,但只要它有 read(self, size: int) -> bytes 方法,就能被当作 Readable 使用(在类型层面)。
关键点:
-
Protocol类本身不能被实例化(TypeError: Protocols cannot be instantiated) - 协议中定义的方法默认是抽象的,但**不要加
@abstractmethod** ——Protocol自己处理抽象性 - 协议支持继承,也支持
runtime_checkable(仅限简单协议,且性能有损) - 如果协议里有可选方法,必须用
typing.runtime_checkable+typing.Optional或typing.overload组合,否则运行时isinstance(obj, Proto)永远为False
怎么写一个能被 mypy 识别的最小 Protocol?
以下是最简可用写法,无装饰、无继承、无泛型:
立即学习“Python免费学习笔记(深入)”;
from typing import Protocol <p>class Drawable(Protocol): def draw(self) -> None: ... def bounding_box(self) -> tuple[float, float, float, float]: ...</p><p>def render(shape: Drawable) -> None: shape.draw() # mypy 知道 shape 一定有 draw()
注意三点:
- 方法体必须是
...(省略号),不是pass,也不是函数体;写pass会导致 mypy 忽略该方法声明 - 参数和返回值类型必须显式标注,否则 mypy 不校验调用兼容性
- 实现类不需要显式继承或注册,只要所有协议方法都存在且签名匹配(参数名可不同,但类型和数量必须一致)
为什么 isinstance(x, MyProtocol) 有时返回 False?
因为默认情况下 Protocol **不支持运行时检查**。mypy 能推导,但 isinstance 看不到协议定义。
解决办法:加 @runtime_checkable 装饰器,但有代价:
from typing import Protocol, runtime_checkable <p>@runtime_checkable class Serializable(Protocol): def serialize(self) -> bytes: ...
此时 isinstance(obj, Serializable) 才可能为 True,但前提是:
- obj 是普通类实例(不是内置类型如
dict、list) - obj 的类动态拥有所有协议方法(不能靠
__getattr__动态提供) - 方法必须是可直接访问的属性(即不是通过
__get__描述符间接暴露) - 性能下降明显 ——
isinstance会遍历协议所有方法并做hasattr检查
Protocol 和 ABC 的关键区别在哪?
别混淆 Protocol 和 ABC(抽象基类)。它们目标不同:
-
ABC是运行时强制的接口契约(靠metaclass=ABCMeta和@abstractmethod),失败在实例化或调用时抛异常 -
Protocol是纯静态类型契约,失败只在 mypy/pyright 报告,运行期零干预 - 你可以让一个类同时继承
ABC并满足某个Protocol,但两者不互相替代 - 泛型协议(如
class Mapper[T, U](Protocol): ...)支持协变/逆变标记(Contravariant),而 ABC 泛型需额外处理
最易忽略的一点:协议方法的参数名不参与匹配(结构匹配只看类型和顺序),但 ABC 的抽象方法名必须严格一致 —— 这正是“结构” vs “名义”的分水岭。


















