Python之Await 协议
一、先澄清await 到底在干什么很多初学者会把 await 理解成“阻塞到结果返回”。这个理解不准确。在同步代码里函数调用通常意味着当前线程一路执行到底中间不能主动把控制权让给别的任务而在异步代码里await 的含义更接近当前协程运行到这里先暂停自己。把“我依赖的那个异步对象”交给事件循环。事件循环去执行别的任务。等这个对象完成后再回来恢复当前协程。恢复点就在 await 之后。所以await 的核心不是“等待”而是“协作式让出执行权”。看一个最基本的例子importasyncioasyncdeffetch_data():print(开始请求)awaitasyncio.sleep(1)print(请求结束)return{name:alice}asyncdefmain():resultawaitfetch_data()print(result)asyncio.run(main())这里真正发生的事情是调用 fetch_data() 时不会立刻把整个函数跑完而是先得到一个协程对象。main 中的 await fetch_data() 会驱动这个协程对象执行。当 fetch_data 内部又遇到 await asyncio.sleep(1) 时fetch_data 自己也会挂起。事件循环此时可以去做别的事情。1 秒后sleep 对应的等待对象完成fetch_data 恢复执行最终把结果返回给 main。所以await 是“挂起与恢复”的语言级入口。二、await 右边到底能放什么不是所有对象都能被 await。Python 只接受“awaitable object”即可等待对象。可等待对象主要分三类原生协程对象Future / Task 一类的对象实现了await方法的自定义对象先看第一类。1. 原生协程对象由 async def 定义的函数调用之后返回的是协程对象。asyncdefwork():return42objwork()print(type(obj))这里的 obj 就是协程对象它可以被 await。注意协程函数和协程对象不是一个东西。async def work: 协程函数work(): 协程对象这和普通函数与函数调用结果的区别类似只不过协程对象不是结果值而是“尚未执行完成的异步计算描述”。2. Task 和 Future在 asyncio 体系里Future 表示“未来某个时刻会产生结果”Task 是“被事件循环调度执行的协程”。importasyncioasyncdefworker():awaitasyncio.sleep(1)returndoneasyncdefmain():taskasyncio.create_task(worker())resultawaittaskprint(result)asyncio.run(main())这里 task 不是协程函数也不是普通值而是一个 Task 对象。它之所以能出现在 await 右边是因为它本身是 awaitable。3. 实现了await的对象这是 await 协议真正的核心。只要对象实现了await并且实现方式符合规范它就可以被 await。importasyncioclassDelay:def__init__(self,seconds,value):self.secondsseconds self.valuevaluedef__await__(self):returnasyncio.sleep(self.seconds,resultself.value).__await__()asyncdefmain():resultawaitDelay(1,hello)print(result)asyncio.run(main())这里 Delay 不是协程对象也不是 Task但因为它实现了await所以仍然可以被 await。三、await 协议的正式定义是什么严格地说await 协议的核心要求是一个对象若想被 await那么它必须是以下之一原生协程对象生成器型协程对象实现了await方法并且该方法返回一个迭代器的对象第三点尤其重要。很多人只记住“实现await就行”这是不完整的。真正的要求是await必须返回一个迭代器而不是任意对象。例如下面这个写法是错误的classBadAwaitable:def__await__(self):return123因为 123 不是迭代器所以 await BadAwaitable() 会报错。正确的方式必须返回一个可迭代推进的对象最常见做法是直接复用另一个 awaitable 的await结果importasyncioclassGoodAwaitable:def__await__(self):returnasyncio.sleep(1,resultok).__await__()这背后的原因是解释器在执行 await 时需要一个“可逐步推进”的对象来承载挂起点和恢复点而这个载体就是迭代器。四、为什么await要返回迭代器这要追溯到 Python 异步模型的底层实现思路。从语义上说协程的挂起与恢复和生成器有非常强的亲缘关系。生成器依靠 yield 暂停下一次 next 或 send 时恢复协程虽然语法层面写成了 async / await但底层仍然沿用了“迭代推进状态机”的思想。因此await 并不是魔法。可以把它理解成一种受限、专用、语义更清晰的协程委托机制。其底层逻辑与生成器时代的 yield from 有很深的继承关系。概念上可以把resultawaitobj近似理解为“取出 obj 对应的 await 迭代器把当前协程的控制流委托给它直到它结束再拿到最终结果。”当然这只是帮助理解的近似模型不是源码级等价翻译但非常接近真实语义。五、一个自定义 awaitable 应该怎样写最稳妥的工程做法通常不是自己手搓底层状态机而是把已有的 awaitable 组合进去。例如我们封装一个“延迟后返回结果”的对象importasyncioclassSleepThen:def__init__(self,delay,value):self.delaydelay self.valuevaluedef__await__(self):asyncdef_inner():awaitasyncio.sleep(self.delay)returnself.valuereturn_inner().__await__()asyncdefmain():valueawaitSleepThen(0.5,{status:ok})print(value)asyncio.run(main())这个实现有两个优点语义清晰不需要直接手动操纵生成器协议细节如果你硬要自己写得更底层也可以例如importasyncioclassSleepThenLowLevel:def__init__(self,delay,value):self.delaydelay self.valuevaluedef__await__(self):yieldfromasyncio.sleep(self.delay).__await__()returnself.valueasyncdefmain():valueawaitSleepThenLowLevel(0.5,done)print(value)asyncio.run(main())这里已经非常接近协议本体了yield from 把控制权委托给另一个等待对象等它完成以后返回最终值这个写法能更直观地展示await 协议本质上是“以迭代器为载体的挂起协议”。六、await 的结果值是怎么回来的这一点必须讲清因为它能把“挂起”和“返回值”统一起来。当你写resultawaitsome_obj你得到的 result并不是 some_obj 本身而是“这个 awaitable 完成后产出的最终值”。例如importasyncioasyncdefcompute():awaitasyncio.sleep(0.2)return100asyncdefmain():xawaitcompute()print(x)asyncio.run(main())输出是 100。从底层协议视角看这个“最终值”在迭代器语义里通常体现为终止时携带的值。对生成器模型熟悉的人会知道生成器结束时可以通过 StopIteration.value 把值带出来。Python 的协程模型沿用了这一思路只是语言层已经把细节包装掉了。所以await 做的不是“读取某个对象的字段”而是“驱动某个异步状态机直到完成并提取它的完成值”。七、事件循环在 await 协议里扮演什么角色await 协议本身只规定“对象如何可等待”并不直接等于“调度机制”。真正负责调度的是事件循环。这点很关键。因为很多人会把 await 和 asyncio 混为一谈实际上两者分工不同await 是语言机制定义了可等待语义asyncio 是标准库中的事件循环与调度实现之一看一个更接近底层的例子importasyncioasyncdefwait_on_future():loopasyncio.get_running_loop()futureloop.create_future()defcomplete():print(设置 Future 结果)future.set_result(future done)loop.call_later(1,complete)print(开始等待)resultawaitfutureprint(拿到结果:,result)asyncio.run(wait_on_future())这里的关键流程是创建一个 Future当前协程 await 这个 Future协程挂起事件循环继续运行别的回调1 秒后 complete 被调用Future 被标记完成事件循环恢复先前挂起的协程await 表达式返回 “future done”由此可以看出await 协议告诉解释器“这个对象可以挂起我”而事件循环负责“什么时候恢复你”。八、Task 为什么能被 awaitTask 很值得单独讲因为它正好体现了“协议”和“调度”的结合。importasyncioasyncdefworker():awaitasyncio.sleep(1)returnworker resultasyncdefmain():taskasyncio.create_task(worker())print(task 已创建)resultawaittaskprint(result)asyncio.run(main())这里 worker() 先产生协程对象create_task 再把它包装成 Task。Task 由事件循环推进执行因此Task 是协程运行的调度单元Task 也是 awaitableawait task 的含义不是“把 task 当函数调”而是“等待 task 对应的执行单元结束并取出它的结果”所以 Task 是异步执行的“运行态对象”协程对象更像“待运行描述”。九、await 与 inspect.isawaitable 的关系工程里经常要判断一个对象能不能 await这时不能只看它是不是协程对象。错误写法importinspectdefonly_coroutine(obj):returninspect.iscoroutine(obj)这会漏掉很多合法 awaitable比如 Task、Future、自定义实现了await的对象。更合理的方式是importinspectimportasyncioclassCustom:def__await__(self):returnasyncio.sleep(0).__await__()asyncdefdemo():coroasyncio.sleep(0)taskasyncio.create_task(asyncio.sleep(0))customCustom()print(inspect.isawaitable(coro))# Trueprint(inspect.isawaitable(task))# Trueprint(inspect.isawaitable(custom))# Trueawaitcoroawaittaskawaitcustom asyncio.run(demo())因此在动态调度、依赖注入、框架执行器里通常应当优先使用 inspect.isawaitable而不是只检查 inspect.iscoroutine。十、await 只能出现在什么位置这是语法层面的硬约束。在现代 Python 中await 只能出现在 async def 定义的函数体内。也就是说await 本身不是一个可以到处随便写的表达式它只能存在于“异步上下文”中。例如下面是非法的importasyncioawaitasyncio.sleep(1)在普通 Python 脚本顶层这会报语法错误。正确写法通常是importasyncioasyncdefmain():awaitasyncio.sleep(1)asyncio.run(main())不过在某些交互环境里比如部分 notebook 或 REPL会支持顶层 await这是宿主环境额外提供的能力不是普通脚本的默认行为。十一、await 和普通函数调用的根本差别看这段代码defsync_add(a,b):returnabasyncdefasync_add(a,b):returnab调用它们xsync_add(1,2)yasync_add(1,2)print(x)# 3print(y)# 协程对象不是 3这说明普通函数调用后直接得到结果协程函数调用后得到的是“未来可产出结果的对象”必须再经过 await才能得到最终值importasyncioasyncdefasync_add(a,b):returnabasyncdefmain():yawaitasync_add(1,2)print(y)# 3asyncio.run(main())所以await 不是“异步函数调用语法糖”而是“异步结果兑现机制”。十二、异常如何穿过 await 传播await 不只是传值也会传异常。importasyncioasyncdefbad():awaitasyncio.sleep(0.1)raiseValueError(boom)asyncdefmain():try:awaitbad()exceptValueErrorasexc:print(捕获到异常:,exc)asyncio.run(main())这里 bad 内部抛出的异常会在 await bad() 处重新表现出来。这说明 await 的语义和普通函数调用很像的一点在于正常完成时返回值向上传播出错时异常向上传播只是它们之间多了一层“挂起与恢复”。十三、取消是 await 协议在调度层的重要延伸在 asyncio 中任务可以被取消这通常通过 CancelledError 体现。importasyncioasyncdefworker():try:print(开始工作)awaitasyncio.sleep(10)exceptasyncio.CancelledError:print(收到取消请求)raiseasyncdefmain():taskasyncio.create_task(worker())awaitasyncio.sleep(0.2)task.cancel()try:awaittaskexceptasyncio.CancelledError:print(task 已取消)asyncio.run(main())这里的重点不是“取消 API 怎么用”而是理解其与 await 的关系当前协程 await 某个 Task如果该 Task 被取消那么 await 表达式不会正常产出结果它会把取消异常传播出来这说明 await 协议并不只承载“最终成功值”也承载“失败与取消”的控制流。十四、await 与 yield from 的关系如果想真正理解 await 协议必须知道它和 yield from 的历史关系。在 async / await 语法出现前Python 曾使用“基于生成器的协程”。当时异步协作常通过 yield from 来表达。现代 Python 里await 可以理解为对异步委托的专门化语法它比 yield from 更严格、更清晰原因包括await 只能作用于 awaitable而不是任意可迭代对象async def 明确标记了协程函数避免与普通生成器混淆语义上更聚焦于异步等待而不是一般性的迭代委托所以await 不是凭空创造的新机制而是在生成器语义基础上为异步编程建立的专用协议层。十五、一个更接近“协议本体”的最小示例下面这个例子能帮助你从“语言糖”退回到“协议视角”。importasyncioclassOneShotValue:def__init__(self,value):self.valuevaluedef__await__(self):ifFalse:yieldreturnself.valueasyncdefmain():resultawaitOneShotValue(123)print(result)asyncio.run(main())这个例子看起来有些奇怪尤其是ifFalse:yield它的作用是让这个函数在语法上成为生成器从而返回一个迭代器对象。虽然不会真的 yield 出什么但协议上它已经满足“返回迭代器”的要求了因此 await 能工作。这个例子很好地说明了一件事await 关心的不是“这个对象是不是某个具体类”而是“它能否通过迭代器协议表达挂起/完成语义”。不过这种写法更适合教学不适合生产环境。工程里一般应当复用已有 awaitable而不是手写这种技巧性实现。十六、什么不属于 await 协议有几个概念很容易混淆必须分开。1. 异步迭代不是 await 协议本身异步迭代使用的是aiteranext对应语法是 async for而不是 await。2. 异步上下文管理不是 await 协议本身异步上下文管理使用的是aenteraexit对应语法是 async with而不是 await。3. 普通可迭代对象不等于 awaitable一个对象能被 for 遍历不代表它能被 await。await 要求的是 awaitable 协议不是 iterable 协议。例如列表、普通生成器都不能直接 await。十七、工程上最常见的几个误区1. 误以为调用 async def 就已经执行了错误理解“我调用了异步函数所以它开始运行了。”实际上单纯调用 async def 函数只会得到协程对象不保证执行。执行通常要通过以下方式之一触发被 await被 create_task 包装被 asyncio.run 作为入口运行2. 误以为 await 会阻塞整个线程不准确。await 会挂起当前协程但只要底层等待对象是非阻塞式的事件循环仍然可以在同一线程调度其他任务。3. 在自定义await里返回了错误类型最典型错误就是返回普通值、列表、协程函数本身而不是迭代器。4. 把“能调用”误认为“能 await”可调用对象和可等待对象是两个不同维度。函数对象未必能 awaitawaitable 未必可调用十八、如何从框架角度理解 await 协议如果你站在框架作者的角度看await 协议的价值非常大因为它提供了一层统一抽象不管右边是原生协程TaskFuture自定义 awaitable只要符合协议调用方都可以统一写成resultawaitobj这意味着上层业务逻辑不需要关心底层异步对象的具体类型只需要关心它是否满足 awaitable 语义。这也是为什么“协议”比“类继承”更重要。Python 在这里采用的是典型的鸭子类型哲学你不必继承某个统一基类只要行为符合协议即可。十九、一个完整示例自定义 awaitable、Task、异常传播放在一起importasyncioimportinspectclassResourceLoader:def__init__(self,name,delay):self.namename self.delaydelaydef__await__(self):asyncdef_load():awaitasyncio.sleep(self.delay)ifself.namebad:raiseRuntimeError(resource load failed)return{resource:self.name}return_load().__await__()asyncdefconsume(obj):ifnotinspect.isawaitable(obj):raiseTypeError(对象不可等待)returnawaitobjasyncdefmain():taskasyncio.create_task(consume(ResourceLoader(user,0.5)))resultawaittaskprint(result)try:awaitconsume(ResourceLoader(bad,0.1))exceptRuntimeErrorasexc:print(异常传播成功:,exc)asyncio.run(main())这段代码展示了四件事自定义对象可以通过await变成 awaitableinspect.isawaitable 可以做统一判定Task 可以包装异步工作并被 await异常会从底层 awaitable 传播到外层 await 表达式二十、可以把 await 协议总结成什么可以把整个 await 协议总结为下面四条await 作用的对象必须是 awaitable。自定义 awaitable 的关键是实现await。await的返回值必须是迭代器而不是任意对象。await 的执行语义是挂起当前协程等待目标对象完成然后返回结果或抛出异常。如果再进一步压缩成一句更偏底层的话await 协议就是“用迭代器描述异步挂起点并由事件循环负责恢复执行”的一套语言约定。二十一、学习这部分时最值得建立的心智模型建议你把 Python 异步系统拆成三层看语法层async def、await、async for、async with协议层await、aiter、anext、aenter、aexit调度层asyncio event loop、Future、Task、回调恢复其中await 协议位于“语法层”和“调度层”之间。它既不是纯语法糖也不是完整调度器它是把“可挂起对象”接入异步执行体系的桥。这一层真正理解了很多问题都会自然变得清楚为什么协程函数调用后不是结果值为什么 Task 能被 await为什么自定义对象实现await就能接入系统为什么 await 的本质是挂起与恢复而不是线程阻塞