Python typing.Protocol配合泛型描述可调用对象的方式
当一个函数既可能接收普通函数,也可能接收带 __call__ 的对象时,单写 Callable[..., T] 往往太宽:输入类型之间的关系表达不出来,关键字参数和自定义调用约束也不好保留。更稳的做法是用 typing.Protocol 定义一个泛型回调协议,把“能接收什么、会返回什么”写成静态接口。
官方地址:https://docs.python.org/3/library/typing.html
泛型可调用协议的核心是一个带类型参数的 __call__:输入类型放在参数位置,返回类型放在结果位置;函数或对象只要满足这份结构,就能被静态类型检查器接受,不需要显式继承协议。
- 用
Protocol+__call__描述回调,比无约束的Callable[..., T]更能保留参数关系。 - 可调用对象的输入类型通常标记为逆变,输出类型标记为协变,替换关系才不会反过来。
- 普通函数、闭包和实现
__call__的实例都可以结构化匹配同一个协议。
先把可调用对象抽象成泛型协议
假设业务函数接收一批原始记录,并把每条记录转换成另一种类型。不要把处理函数写成接受任意参数的黑盒,而是把输入和输出的关系放进协议:
from collections.abc import Iterable
from typing import Protocol, TypeVar
InputT = TypeVar("InputT", contravariant=True)
OutputT = TypeVar("OutputT", covariant=True)
class Converter(Protocol[InputT, OutputT]):
# __call__ 是回调协议的静态入口,描述一次转换的输入和输出。
def __call__(self, value: InputT) -> OutputT:
...
def convert_all(
values: Iterable[InputT],
converter: Converter[InputT, OutputT],
) -> list[OutputT]:
# 处理函数只依赖协议,不依赖某一个转换器类。
return [converter(value) for value in values]
InputT 出现在可调用对象的参数位置,所以使用 contravariant=True;OutputT 出现在返回值位置,所以使用 covariant=True。这不是为了改变运行时行为,而是告诉类型检查器如何判断一个更宽或更窄的回调能否替换当前回调。

让函数与可调用实例共享同一份接口
结构化子类型的价值在于,实现方不需要为了“满足接口”修改继承关系。下面的普通函数和类实例都具备兼容的调用签名,因此可以交给同一个 convert_all:
class UserRecord:
def __init__(self, name: str) -> None:
self.name = name
def normalize_name(value: UserRecord) -> str:
# 普通函数天然可以作为 Converter[UserRecord, str] 使用。
return value.name.strip().casefold()
class DisplayName:
def __call__(self, value: UserRecord) -> str:
# 可调用实例可以保存配置,同时仍符合同一个 __call__ 契约。
return value.name.strip().title()
records = [UserRecord(" Ada "), UserRecord("grace")]
normalized = convert_all(records, normalize_name)
displayed = convert_all(records, DisplayName())
这里不需要写 class DisplayName(Converter[UserRecord, str])。类型检查器会检查它是否有匹配的 __call__。如果把参数名、参数种类或返回类型改得不兼容,错误会出现在调用点附近,而不是等到业务运行后才暴露。
Python 3.12 与旧语法的迁移写法
Python 3.12 引入了类型参数语法,可以把类型变量直接放在函数和协议定义上;项目仍需兼容 Python 3.11 时,继续使用 TypeVar 工厂更稳妥。两种写法表达的是同一个类型关系:
# Python 3.12:类型参数更靠近声明,适合新项目。
class Converter12[InputT, OutputT](Protocol):
def __call__(self, value: InputT) -> OutputT:
...
def convert_one12[T, R](value: T, converter: Converter12[T, R]) -> R:
# 返回值 R 与传入 converter 的输出类型保持关联。
return converter(value)
# Python 3.11 及更早版本:复用前面的 InputT、OutputT 定义。
class Converter311(Protocol[InputT, OutputT]):
def __call__(self, value: InputT) -> OutputT:
...
不要把两种声明方式混在同一个协议里,也不要为了兼容旧版本而把所有参数退化成 Any。如果库需要支持多个解释器版本,可以在类型定义文件中保留旧语法,应用代码再按最低支持版本选择写法。
| 需求 | 建议声明 | 边界 |
|---|---|---|
| 固定一个参数和返回值 | Protocol + 泛型 __call__ | 能表达输入输出的对应关系 |
| 参数名、关键字参数或重载很复杂 | 回调协议继续扩展 __call__ | 签名必须与实现的参数种类兼容 |
| 只关心返回值类型 | Callable[..., OutputT] | 参数约束会被主动放宽 |
| 运行时判断是否满足协议 | 谨慎使用 @runtime_checkable | 运行时检查只看有限的成员存在性,不等于静态类型检查 |
把静态检查和运行时职责分开
Protocol 主要服务静态分析。它不能像普通类一样被实例化,也不会在程序运行时自动验证泛型参数。提交代码前可以让 mypy 或 pyright 检查以下三件事:调用参数是否匹配、返回值是否真的能赋给目标类型、对象的 __call__ 是否使用了兼容的参数名和参数种类。
# 这个示例故意保留类型关系,交给静态检查器报告错误。
class BadConverter:
def __call__(self, value: int) -> bytes:
# 返回 bytes,不能假装成 Converter[int, str]。
return str(value).encode("utf-8")
bad: Converter[int, str] = BadConverter() # 类型检查器应在这里报错。
如果业务确实需要运行时校验,应单独检查输入数据或使用明确的运行时校验库,不要把 Protocol 当成数据验证器。这样可以让静态接口负责开发期约束,让业务代码负责运行时异常和用户输入。

常见问题
为什么不用一个 Callable 就结束?
简单回调用 Callable[[T], R] 足够;当需要关键字参数、可变参数、重载或可调用对象的更精细签名时,带 __call__ 的协议更容易表达,也更方便扩展成员。
泛型 Protocol 一定要显式继承吗?
不需要。实现方只要提供兼容的成员就能结构化匹配;显式继承主要用于表达意图或让工具在定义处更早发现不一致。
为什么输入类型要逆变?
一个能处理更宽输入范围的函数,可以替代只要求较窄输入范围的函数;这是可调用参数的安全替换方向。返回值则相反,能够返回更具体类型的实现可以替代返回更宽类型的实现。
Go time.LoadLocation在精简容器中失败的部署处理
- 上一篇
- Go time.LoadLocation在精简容器中失败的部署处理
- 下一篇
- Go http.ServeContent实现范围请求时的文件时间处理要点
-
- 文章 · python教程 | 2小时前 |
- Python sqlite3事务提交与异常回滚的上下文写法
- 357浏览 收藏
-
- 文章 · python教程 | 3小时前 |
- Python dataclass用field配置默认工厂的对象设计
- 114浏览 收藏
-
- 文章 · python教程 | 4小时前 | 并发 · python · 异步编程 · asyncio TaskGroup ExceptionGroup
- Python asyncio.TaskGroup组织并发任务与异常取消
- 322浏览 收藏
-
- 文章 · python教程 | 5小时前 | python · pathlib ·
- Python pathlib批量改名并保留冲突回滚点的脚本
- 422浏览 收藏
-
- 文章 · python教程 | 8小时前 |
- Python argparse让位置参数与子命令独立解析的实现方法
- 298浏览 收藏
-
- 文章 · python教程 | 9小时前 | 并发 · python · logging · 异步日志 QueueHandler QueueListener Python logging
- Python logging用 QueueHandler 隔离日志 I/O的实现方法
- 323浏览 收藏
-
- 文章 · python教程 | 11小时前 | 并发 · 线程池 · 异常处理 · python · Python threadpoolexecutor future concurrent.futures
- Python concurrent收集线程池异常并关闭执行器的实现方法
- 262浏览 收藏
-
- 文章 · python教程 | 12小时前 | 性能优化 · 缓存设计 · Python教程 · Python functools.lru_cache Python 可变参数缓存键 Python list dict 缓存 Python 缓存失效
- Python functools避免把可变参数放进缓存键的实现方法
- 344浏览 收藏
-
- 文章 · python教程 | 13小时前 |
- Python typing用 TypeGuard 缩小联合类型的实现方法
- 290浏览 收藏
-
- 文章 · python教程 | 14小时前 | python ·
- Python dataclass用 slots 控制实例字段开销的实现方法
- 118浏览 收藏
-
- 文章 · python教程 | 16小时前 |
- Python sqlite3用 detect_types 转换日期字段的实现方法
- 159浏览 收藏
-
- 文章 · python教程 | 4天前 |
- Python json解析金额 JSON 保留 Decimal的实现方法
- 337浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 135次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 200次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 146次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 126次使用
-
- CMMLU
- 深入了解CMMLU中文评估基准,涵盖67个学科主题,提供数据集下载、Zero-shot/Five-shot评估方法及排行榜,助力优化中文语言模型性能。
- 111次使用
-
- Go 泛型函数如何设计可读的约束:类型集、接口方法与调用方推断边界
- 2026-08-25 242浏览
-
- Go go/types.Info.FileVersions 如何读取单文件语言版本:类型检查配置与语法兼容边界
- 2026-08-30 440浏览
-
- Python asyncio TaskGroup 实战:别让超时请求留下后台任务
- 2026-06-02 496浏览
-
- Python free-threaded CPython 实战:别急着线上关 GIL
- 2026-06-03 381浏览
-
- Python Pydantic v2 实战:TypeAdapter 别在请求里反复造
- 2026-06-03 342浏览

