当前位置:首页 > 文章列表 > 文章 > python教程 > Python Protocol 怎样描述带异步方法的结构类型

Python Protocol 怎样描述带异步方法的结构类型

来源:17golang原创 2026-10-09 16:49:54 0浏览 收藏

给异步仓储、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

异步 Protocol 排错顺序
  1. 先确认调用方到底是直接调用还是使用 await。
  2. 协议中优先用 async def -> T 描述 await 后得到 T。
  3. 检查实现方法是否误写为 async def -> Awaitable[T]。
  4. 逐项对齐参数类型、参数名、位置参数与关键字参数。
  5. 用静态赋值断言检查结构实现,不依赖运行时 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。

Python Protocol 异步方法从调用到协程再到 await 结果的静态类型关系图
图1:async def 的返回注解描述 await 后结果,调用表达式本身产生协程对象。

第二层检查:不要给 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 不要求实现使用同一个类层次。

Python 异步 Protocol 从 await 结果、参数形状、泛型到运行时检查的静态排错图
图2:从 await 后结果开始,依次检查 Awaitable 层级、参数形状、泛型方向和运行时边界。

第六层检查: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 层级、参数形状、泛型方向和运行时边界逐层排查。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
multipart 表单读取时报 message too large 怎么处理multipart 表单读取时报 message too large 怎么处理
上一篇
multipart 表单读取时报 message too large 怎么处理
用 MultipartReader 限制每个表单字段的读取量
下一篇
用 MultipartReader 限制每个表单字段的读取量
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    393次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    471次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    478次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    421次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    247次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码