Python Protocol 与结构化类型
Protocol 在类型检查层表达结构要求,实现静态鸭子类型,与运行时鸭子类型互补。
#type / concept
#status / growing
#tech / dev
#resource / python
#tech / lang / python
[!info] 关联笔记
Python Protocol 与结构化类型
这个概念为什么出现
运行时鸭子类型灵活,但公共库希望在 CI 就发现“少了 close 方法”。typing.Protocol 让类型检查器按结构判定兼容,无需名义继承。
[!abstract] 一句话理解 Protocol 定义一组必须具备的方法/属性;类型检查器对结构匹配的类型放行,即使它们未显式继承该 Protocol。
最小可运行示例
先把示例放进业务场景,再看代码:
场景:关闭资源的统一收尾函数
# protocol_closer.py
# 业务意图:close_all 接受任何带 close() 的对象。
# 教学点:
# - Protocol 定义结构;
# - 不同类结构兼容;
# - 运行时不必继承 Protocol(除非 runtime_checkable)。
from typing import Protocol
class Closable(Protocol):
def close(self) -> None: ...
class DbConn:
def close(self) -> None:
print("db closed")
class FileHandle:
def close(self) -> None:
print("file closed")
def close_all(resources: list[Closable]) -> None:
for r in resources:
r.close()
def main() -> None:
close_all([DbConn(), FileHandle()])
if __name__ == "__main__":
main()
建议运行:
python protocol_closer.py
期望输出:
db closed
file closed
结合场景再看三个关注点
- DbConn/FileHandle 无共同基类仍可协作。
- 静态检查器会拒绝缺
close的类型传入。 - 需要运行时
isinstance再用@runtime_checkable(有限制)。
核心概念与准确模型
- 结构子类型 vs 名义子类型
Protocol成员用...占位- 可泛型 Protocol
- 与 ABC 虚拟子类不同层
边界情况与反直觉行为
- 仅属性/方法集合匹配——语义仍需文档。
- runtime_checkable 只查存在性,不查签名细节。
- 回调协议用
Callable有时更简单。
常见误区
[!warning] 常见误区:Protocol 会改变运行时继承 错误理解:必须 subclass Protocol。
正确模型:默认是静态契约;运行时对象无需知道 Protocol。
工程实践
- 公共插件点用 Protocol 文档化
- 与 pyright 严格模式配合
- 小而稳的协议优于大而全
本节总结
Protocol 把鸭子类型提升到可检查的工程实践,不牺牲组合灵活性。
自测题
- Protocol 与 ABC 强制继承差别?
- 为什么还要测试?
参考答案
- Protocol 结构兼容即可;ABC 常要名义注册/继承。
- 结构匹配不等于业务语义正确。
延伸阅读与资料来源
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| Protocol | 文档 | API |
| PEP 544 | PEP | Protocols |