Python Protocol 怎样描述带异步方法的结构类型
给异步仓储、HTTP 客户端或插件接口做类型约束时,常见需求是“只要对象拥有指定的异步方法,就可以传进来”,而不是要求所有实现都继承同一个基类。typing.Protocol 正适合这种结构类型:类型检查器比较对象实际提供的成员和签名,不要求显式继承。
直接答案:如果调用方写的是 user = await source.fetch(id),协议通常应声明 async def fetch(...) -> User: ...。这里的 User 表示 await 之后 的结果,不需要再包一层 Awaitable[User]。
官方文档:https://docs.python.org/3/library/typing.html#typing.Protocol
- 先确认调用方到底是直接调用还是使用
await。 - 协议中优先用
async def -> T描述 await 后得到 T。 - 检查实现方法是否误写为
async def -> Awaitable[T]。 - 逐项对齐参数类型、参数名、位置参数与关键字参数。
- 用静态赋值断言检查结构实现,不依赖运行时 Protocol 判断签名。
现象:方法明明存在,类型检查仍报不兼容
假设业务层只关心“能按用户 ID 异步取回用户”的对象。一个实现从数据库读取,另一个实现调用远程服务,它们不需要共享父类。
from dataclasses import dataclass
from typing import Protocol
@dataclass(frozen=True)
class User:
id: int
name: str
class UserSource(Protocol):
async def fetch(self, user_id: int) -> User:
# 省略号表示协议只描述成员契约
...
async def show_name(source: UserSource, user_id: int) -> str:
# fetch 的调用结果可等待,await 后得到 User
user = await source.fetch(user_id)
return user.name
任何提供兼容 fetch() 签名的类都能作为 UserSource,无需写 class SqlUserSource(UserSource)。这就是静态鸭子类型:类型检查器关心结构,不关心名义继承。
第一层检查:async def 的返回注解表示什么
async def fetch(...) -> User 并不是说调用 fetch() 立即得到 User。调用会创建协程对象,await 该对象后才得到 User。因此协议里的返回注解写业务结果类型即可。
class SqlUserSource:
async def fetch(self, user_id: int) -> User:
# 真实项目可以在这里等待异步数据库驱动
await asyncio.sleep(0)
return User(id=user_id, name="Ada")
source: UserSource = SqlUserSource()
# 这行赋值是静态契约哨兵;实现类无需继承协议
若编辑器把 source.fetch(1) 推断为 Coroutine[Any, Any, User],这是正常现象;协程实现了可等待协议,await 后结果才是 User。

第二层检查:不要给 async def 多包一层 Awaitable
最常见的错误是把协议写成 async def fetch(...) -> Awaitable[User]。这个签名的含义不是“fetch 可等待”,而是“await fetch 后还会得到另一个 Awaitable”。类型检查器因此可能把最终值推断成还需要再 await 一次的对象。
from collections.abc import Awaitable
class WrongSource(Protocol):
async def fetch(self, user_id: int) -> Awaitable[User]:
# 错误契约:第一次 await 后仍要求得到 Awaitable[User]
...
async def wrong_use(source: WrongSource) -> User:
pending_user = await source.fetch(1)
# pending_user 仍是 Awaitable[User],需要第二次 await 才是 User
return await pending_user
只有当异步函数确实返回另一个延迟对象时才这样声明。普通异步接口直接写 async def -> User。
什么时候使用 def 返回 Awaitable
有时接口想表达的是“调用后返回任意可等待对象”,但并不要求实现必须用 async def。此时可以把协议成员写成普通 def,返回 Awaitable[T]。这样实现可以返回协程、Future 或自定义 Awaitable。
from collections.abc import Awaitable
class DeferredUserSource(Protocol):
def fetch(self, user_id: int) -> Awaitable[User]:
# 只约束调用结果可等待,不约束实现采用 async def
...
class TaskUserSource:
def fetch(self, user_id: int) -> asyncio.Task[User]:
# 把协程调度成 Task,Task 也是 Awaitable[User]
return asyncio.create_task(self._load(user_id))
async def _load(self, user_id: int) -> User:
# 私有协程负责实际异步加载
await asyncio.sleep(0)
return User(user_id, "Grace")
选择标准很简单:要强调“这是异步方法”,用协议中的 async def;要允许任何返回可等待对象的调用形式,用 def -> Awaitable[T]。两者的调用端都可以 await,但接口约束范围不同。
第三层检查:参数名和参数种类也是协议的一部分
结构匹配不只比较“有几个参数”。如果协议允许关键字调用,参数名就会影响兼容性;位置专用参数、关键字专用参数和默认值也会影响调用集合。调用方能做的每一种合法调用,实现方都必须接得住。
class UserSource(Protocol):
async def fetch(
self,
user_id: int,
/,
*,
timeout: float | None = None,
) -> User:
# user_id 仅限位置传入,timeout 仅限关键字传入
...
class HttpUserSource:
async def fetch(
self,
user_id: int,
/,
*,
timeout: float | None = None,
) -> User:
# 参数种类和可接受调用方式与协议保持一致
return await self._request(user_id, timeout=timeout)
如果实现把 timeout 改成必填位置参数,调用方按协议写 fetch(1, timeout=0.5) 时就会失败,所以类型检查器拒绝这种实现是有意义的。
第四层检查:异步泛型协议如何保留结果类型
加载器结构相同但结果类型不同,可以把协议做成泛型。Python 3.11 及更早版本可使用 TypeVar;较新的 Python 也支持类型参数语法。只读返回值通常可以使用协变类型变量。
from typing import Protocol, TypeVar
T_co = TypeVar("T_co", covariant=True)
class AsyncLoader(Protocol[T_co]):
async def load(self, key: str) -> T_co:
# T_co 表示 await 后的结果类型
...
async def load_user(loader: AsyncLoader[User]) -> User:
# 类型检查器能够保留具体的 User 结果
return await loader.load("current-user")
如果类型变量同时出现在输入和输出位置,不要机械地标记为协变。此时通常应使用不变类型变量,或把读写能力拆成两个更小的协议。
第五层检查:异步回调应使用 __call__ 协议
Callable[[Event], Awaitable[None]] 能描述简单异步回调;若回调包含关键字专用参数、重载或更复杂调用形状,应定义带 __call__ 的 Protocol。
from typing import Protocol
class EventHandler(Protocol):
async def __call__(
self,
event: User,
*,
retry: bool = False,
) -> None:
# 协议同时约束异步结果和关键字参数 retry
...
async def dispatch(handler: EventHandler, event: User) -> None:
# 调用对象本身,await 后没有业务返回值
await handler(event, retry=True)
函数、实现了异步 __call__ 的对象都可以匹配,只要完整签名可赋值。Protocol 不要求实现使用同一个类层次。

第六层检查:runtime_checkable 不能验证异步签名
@runtime_checkable 只适合做很浅的成员存在检查。官方文档明确说明,它不会检查成员的类型签名。一个对象只要有同名 fetch 属性,就可能通过 isinstance(),即使该方法是同步的、参数不兼容或返回错误类型。
from typing import Protocol, runtime_checkable
@runtime_checkable
class RuntimeSource(Protocol):
async def fetch(self, user_id: int) -> User:
# 装饰器只让协议可用于 isinstance,不验证完整签名
...
class MisleadingSource:
def fetch(self, user_id: str) -> int:
# 名称存在,但参数、返回值和异步性质都不兼容
return 1
assert isinstance(MisleadingSource(), RuntimeSource)
# 运行时结构检查可能为真,静态类型检查仍应报错
因此,异步 Protocol 的主验证手段应是 mypy、Pyright 等静态检查。运行时若必须防御外部插件,可以在边界处调用后使用 inspect.isawaitable() 检查结果,并把不兼容异常转换为明确的插件错误;但这不能替代静态签名检查。
用静态哨兵和最小运行测试反向确认
静态检查负责证明结构兼容,运行测试负责证明实现真的可等待并返回期望值。两类测试不要混在一起。
def accepts_source(source: UserSource) -> None:
# 该函数只用于触发静态结构检查
pass
async def smoke_test() -> None:
source = SqlUserSource()
accepts_source(source)
# 运行测试确认协程可等待且结果符合业务预期
user = await source.fetch(7)
assert user.id == 7
若实现来自第三方库且缺少注解,可以为它写一个类型安全的适配器,而不是在业务代码里到处使用 Any 或 cast()。适配器既统一签名,也集中处理超时、异常与返回数据转换。
最终检查清单
| 检查项 | 正确证据 | 常见修复 |
|---|---|---|
| await 后结果 | async def -> T | 移除多余的 Awaitable[T] |
| 调用形式 | 实现接受协议允许的全部调用 | 对齐参数名、/、* 与默认值 |
| 结构实现 | 赋值或参数传递通过静态检查 | 补齐缺失成员和准确注解 |
| 泛型结果 | await 后保留具体类型 | 正确选择不变或协变 TypeVar |
| 运行时判断 | 只把它当成员存在检查 | 不要用 runtime_checkable 证明签名正确 |
| 行为验证 | 协程可等待且结果正确 | 增加最小异步测试和边界异常测试 |
常见问题
实现类必须继承 Protocol 吗?
不必须。成员和签名兼容即可隐式实现。显式继承适合希望类型检查器更早检查实现完整性,或需要复用协议默认实现的场景。
Protocol 能保证方法运行时一定是协程函数吗?
静态检查器可以根据签名检查调用结果是否可等待;runtime_checkable 本身不能验证异步性质或返回类型。
Callable 和带 __call__ 的 Protocol 怎么选?
简单参数列表用 Callable[..., Awaitable[T]] 即可;需要关键字专用参数、重载或精确参数名时,用 __call__ Protocol。
为什么 async def 返回 T,而不是 Coroutine[T]?
返回注解描述协程执行完成后的结果。调用表达式的类型由类型检查器展开为协程类型,业务代码 await 后得到 T。
描述带异步方法的结构类型,最稳妥的起点是把调用方写出来:如果调用方需要 await obj.method() 后得到 T,就在 Protocol 中声明 async def method() -> T。出现不兼容时,再按 Awaitable 层级、参数形状、泛型方向和运行时边界逐层排查。
multipart 表单读取时报 message too large 怎么处理
- 上一篇
- multipart 表单读取时报 message too large 怎么处理
- 下一篇
- 用 MultipartReader 限制每个表单字段的读取量
-
- 文章 · python教程 | 3小时前 | Python教程 · InitVar __post_init__ Python dataclasses.replace 数据类复制
- Python dataclasses.replace 遇到 InitVar 时怎样传递参数
- 381浏览 收藏
-
- 文章 · python教程 | 6小时前 | python · pathlib ·
- Python importlib.resources.as_file 的临时路径何时失效
- 318浏览 收藏
-
- 文章 · python教程 | 8小时前 | SQLite · Python教程 · Python sqlite3 Connection.backup 进度回调 SQLite备份
- Python sqlite3 备份进度回调怎样判断剩余页数
- 264浏览 收藏
-
- 文章 · python教程 | 10小时前 | python · 内存优化 · Python教程 · 文件读取 大文件处理 Python mmap 分段映射 ALLOCATIONGRANULARITY
- Python mmap 怎样分段处理超过内存的大文件
- 146浏览 收藏
-
- 文章 · python教程 | 12小时前 | python · Python 二进制协议 零拷贝 memoryview
- Python memoryview 如何零拷贝切片二进制协议数据
- 225浏览 收藏
-
- 文章 · python教程 | 14小时前 | 并发控制 · Python教程 · asyncio · 虚假唤醒 wait_for Python asyncio asyncio.Condition 异步同步
- Python asyncio.Condition.wait_for 如何处理虚假唤醒
- 478浏览 收藏
-
- 文章 · python教程 | 16小时前 |
- Python ExceptionGroup 派生新组时如何保留异常元数据
- 417浏览 收藏
-
- 文章 · python教程 | 18小时前 | 异常处理 · 并发编程 · Python教程 · asyncio · asyncio 结构化并发 ExceptionGroup except* Python TaskGroup
- Python TaskGroup 如何汇总多个子任务异常
- 208浏览 收藏
-
- 文章 · python教程 | 21小时前 | 并发编程 · 工程实践 · Python教程 · 多进程日志 QueueListener multiprocessing.Queue RotatingFileHandler Python QueueHandler
- Python 日志 QueueHandler 解决多进程写入争用
- 186浏览 收藏
-
- 文章 · python教程 | 1天前 | 数据校验 · python · Pydantic 部分更新 exclude_unset model_fields_set 显式空值 model_dump
- Pydantic 模型更新时区分未提供字段与显式空值
- 399浏览 收藏
-
- 文章 · python教程 | 1天前 |
- pytest Fixture 作用域如何影响测试隔离与速度
- 341浏览 收藏
-
- 文章 · python教程 | 1天前 | Python教程 · pathlib · 路径安全 Python pathlib Path.resolve 目录穿越 relative_to
- Pathlib 安全拼接用户路径:解析后再验证根目录
- 463浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 393次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 471次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 478次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 421次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 247次使用
-
- Diffusers ControlNet 条件图尺寸匹配的处理
- 2026-10-02 314浏览
-
- Python asyncio TaskGroup 实战:别让超时请求留下后台任务
- 2026-06-02 496浏览
-
- Python free-threaded CPython 实战:别急着线上关 GIL
- 2026-06-03 381浏览
-
- Python Pydantic v2 实战:TypeAdapter 别在请求里反复造
- 2026-06-03 342浏览
-
- Python SQLAlchemy AsyncSession 实战:别在并发任务里共享 Session
- 2026-06-03 340浏览

