python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python Await 协议

Python之Await 协议的实现

作者:无风听海

本文主要介绍了Python之Await 协议的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

一、先澄清:await 到底在干什么

很多初学者会把 await 理解成“阻塞到结果返回”。这个理解不准确。

在同步代码里,函数调用通常意味着当前线程一路执行到底,中间不能主动把控制权让给别的任务;而在异步代码里,await 的含义更接近:

  1. 当前协程运行到这里,先暂停自己。
  2. 把“我依赖的那个异步对象”交给事件循环。
  3. 事件循环去执行别的任务。
  4. 等这个对象完成后,再回来恢复当前协程。
  5. 恢复点就在 await 之后。

所以,await 的核心不是“等待”,而是“协作式让出执行权”。

看一个最基本的例子:

import asyncio


async def fetch_data():
    print("开始请求")
    await asyncio.sleep(1)
    print("请求结束")
    return {"name": "alice"}


async def main():
    result = await fetch_data()
    print(result)


asyncio.run(main())

这里真正发生的事情是:

所以,await 是“挂起与恢复”的语言级入口。

二、await 右边到底能放什么

不是所有对象都能被 await。Python 只接受“awaitable object”,即可等待对象。

可等待对象主要分三类:

  1. 原生协程对象
  2. Future / Task 一类的对象
  3. 实现了 await 方法的自定义对象

先看第一类。

1. 原生协程对象

由 async def 定义的函数,调用之后返回的是协程对象。

async def work():
    return 42

obj = work()
print(type(obj))

这里的 obj 就是协程对象,它可以被 await。

注意:协程函数和协程对象不是一个东西。

这和普通函数与函数调用结果的区别类似,只不过协程对象不是结果值,而是“尚未执行完成的异步计算描述”。

2. Task 和 Future

在 asyncio 体系里,Future 表示“未来某个时刻会产生结果”;Task 是“被事件循环调度执行的协程”。

import asyncio


async def worker():
    await asyncio.sleep(1)
    return "done"


async def main():
    task = asyncio.create_task(worker())
    result = await task
    print(result)


asyncio.run(main())

这里 task 不是协程函数,也不是普通值,而是一个 Task 对象。它之所以能出现在 await 右边,是因为它本身是 awaitable。

3. 实现了await的对象

这是 await 协议真正的核心。只要对象实现了 await,并且实现方式符合规范,它就可以被 await。

import asyncio


class Delay:
    def __init__(self, seconds, value):
        self.seconds = seconds
        self.value = value

    def __await__(self):
        return asyncio.sleep(self.seconds, result=self.value).__await__()


async def main():
    result = await Delay(1, "hello")
    print(result)


asyncio.run(main())

这里 Delay 不是协程对象,也不是 Task,但因为它实现了 await,所以仍然可以被 await。

三、await 协议的正式定义是什么

严格地说,await 协议的核心要求是:

一个对象若想被 await,那么它必须是以下之一:

  1. 原生协程对象
  2. 生成器型协程对象
  3. 实现了 await 方法,并且该方法返回一个迭代器的对象

第三点尤其重要。很多人只记住“实现 await 就行”,这是不完整的。真正的要求是:

await 必须返回一个迭代器,而不是任意对象。

例如下面这个写法是错误的:

class BadAwaitable:
    def __await__(self):
        return 123

因为 123 不是迭代器,所以 await BadAwaitable() 会报错。

正确的方式必须返回一个可迭代推进的对象,最常见做法是直接复用另一个 awaitable 的 await 结果:

import asyncio


class GoodAwaitable:
    def __await__(self):
        return asyncio.sleep(1, result="ok").__await__()

这背后的原因是:解释器在执行 await 时,需要一个“可逐步推进”的对象来承载挂起点和恢复点,而这个载体就是迭代器。

四、为什么 await 要返回迭代器

这要追溯到 Python 异步模型的底层实现思路。

从语义上说,协程的挂起与恢复,和生成器有非常强的亲缘关系。生成器依靠 yield 暂停,下一次 next 或 send 时恢复;协程虽然语法层面写成了 async / await,但底层仍然沿用了“迭代推进状态机”的思想。

因此,await 并不是魔法。可以把它理解成一种受限、专用、语义更清晰的协程委托机制。其底层逻辑与生成器时代的 yield from 有很深的继承关系。

概念上可以把:

result = await obj

近似理解为:

“取出 obj 对应的 await 迭代器,把当前协程的控制流委托给它,直到它结束,再拿到最终结果。”

当然,这只是帮助理解的近似模型,不是源码级等价翻译,但非常接近真实语义。

五、一个自定义 awaitable 应该怎样写

最稳妥的工程做法通常不是自己手搓底层状态机,而是把已有的 awaitable 组合进去。

例如我们封装一个“延迟后返回结果”的对象:

import asyncio


class SleepThen:
    def __init__(self, delay, value):
        self.delay = delay
        self.value = value

    def __await__(self):
        async def _inner():
            await asyncio.sleep(self.delay)
            return self.value

        return _inner().__await__()


async def main():
    value = await SleepThen(0.5, {"status": "ok"})
    print(value)


asyncio.run(main())

这个实现有两个优点:

  1. 语义清晰
  2. 不需要直接手动操纵生成器协议细节

如果你硬要自己写得更底层,也可以,例如:

import asyncio


class SleepThenLowLevel:
    def __init__(self, delay, value):
        self.delay = delay
        self.value = value

    def __await__(self):
        yield from asyncio.sleep(self.delay).__await__()
        return self.value


async def main():
    value = await SleepThenLowLevel(0.5, "done")
    print(value)


asyncio.run(main())

这里已经非常接近协议本体了:

这个写法能更直观地展示:await 协议本质上是“以迭代器为载体的挂起协议”。

六、await 的结果值是怎么回来的

这一点必须讲清,因为它能把“挂起”和“返回值”统一起来。

当你写:

result = await some_obj

你得到的 result,并不是 some_obj 本身,而是“这个 awaitable 完成后产出的最终值”。

例如:

import asyncio


async def compute():
    await asyncio.sleep(0.2)
    return 100


async def main():
    x = await compute()
    print(x)


asyncio.run(main())

输出是 100。

从底层协议视角看,这个“最终值”在迭代器语义里通常体现为终止时携带的值。对生成器模型熟悉的人会知道,生成器结束时可以通过 StopIteration.value 把值带出来。Python 的协程模型沿用了这一思路,只是语言层已经把细节包装掉了。

所以,await 做的不是“读取某个对象的字段”,而是“驱动某个异步状态机直到完成,并提取它的完成值”。

七、事件循环在 await 协议里扮演什么角色

await 协议本身只规定“对象如何可等待”,并不直接等于“调度机制”。真正负责调度的是事件循环。

这点很关键。因为很多人会把 await 和 asyncio 混为一谈,实际上两者分工不同:

看一个更接近底层的例子:

import asyncio


async def wait_on_future():
    loop = asyncio.get_running_loop()
    future = loop.create_future()

    def complete():
        print("设置 Future 结果")
        future.set_result("future done")

    loop.call_later(1, complete)

    print("开始等待")
    result = await future
    print("拿到结果:", result)


asyncio.run(wait_on_future())

这里的关键流程是:

  1. 创建一个 Future
  2. 当前协程 await 这个 Future
  3. 协程挂起
  4. 事件循环继续运行别的回调
  5. 1 秒后 complete 被调用,Future 被标记完成
  6. 事件循环恢复先前挂起的协程
  7. await 表达式返回 “future done”

由此可以看出:

await 协议告诉解释器“这个对象可以挂起我”,而事件循环负责“什么时候恢复你”。

八、Task 为什么能被 await

Task 很值得单独讲,因为它正好体现了“协议”和“调度”的结合。

import asyncio


async def worker():
    await asyncio.sleep(1)
    return "worker result"


async def main():
    task = asyncio.create_task(worker())
    print("task 已创建")
    result = await task
    print(result)


asyncio.run(main())

这里 worker() 先产生协程对象,create_task 再把它包装成 Task。Task 由事件循环推进执行,因此:

所以 Task 是异步执行的“运行态对象”,协程对象更像“待运行描述”。

九、await 与 inspect.isawaitable 的关系

工程里经常要判断一个对象能不能 await,这时不能只看它是不是协程对象。

错误写法:

import inspect

def only_coroutine(obj):
    return inspect.iscoroutine(obj)

这会漏掉很多合法 awaitable,比如 Task、Future、自定义实现了 await 的对象。

更合理的方式是:

import inspect
import asyncio


class Custom:
    def __await__(self):
        return asyncio.sleep(0).__await__()


async def demo():
    coro = asyncio.sleep(0)
    task = asyncio.create_task(asyncio.sleep(0))
    custom = Custom()

    print(inspect.isawaitable(coro))    # True
    print(inspect.isawaitable(task))    # True
    print(inspect.isawaitable(custom))  # True

    await coro
    await task
    await custom


asyncio.run(demo())

因此,在动态调度、依赖注入、框架执行器里,通常应当优先使用 inspect.isawaitable,而不是只检查 inspect.iscoroutine。

十、await 只能出现在什么位置

这是语法层面的硬约束。

在现代 Python 中,await 只能出现在 async def 定义的函数体内。也就是说,await 本身不是一个可以到处随便写的表达式,它只能存在于“异步上下文”中。

例如下面是非法的:

import asyncio

await asyncio.sleep(1)

在普通 Python 脚本顶层这会报语法错误。正确写法通常是:

import asyncio


async def main():
    await asyncio.sleep(1)


asyncio.run(main())

不过在某些交互环境里,比如部分 notebook 或 REPL,会支持顶层 await,这是宿主环境额外提供的能力,不是普通脚本的默认行为。

十一、await 和普通函数调用的根本差别

看这段代码:

def sync_add(a, b):
    return a + b


async def async_add(a, b):
    return a + b

调用它们:

x = sync_add(1, 2)
y = async_add(1, 2)

print(x)  # 3
print(y)  # 协程对象,不是 3

这说明:

必须再经过 await,才能得到最终值:

import asyncio


async def async_add(a, b):
    return a + b


async def main():
    y = await async_add(1, 2)
    print(y)  # 3


asyncio.run(main())

所以,await 不是“异步函数调用语法糖”,而是“异步结果兑现机制”。

十二、异常如何穿过 await 传播

await 不只是传值,也会传异常。

import asyncio


async def bad():
    await asyncio.sleep(0.1)
    raise ValueError("boom")


async def main():
    try:
        await bad()
    except ValueError as exc:
        print("捕获到异常:", exc)


asyncio.run(main())

这里 bad 内部抛出的异常,会在 await bad() 处重新表现出来。

这说明 await 的语义和普通函数调用很像的一点在于:

只是它们之间多了一层“挂起与恢复”。

十三、取消是 await 协议在调度层的重要延伸

在 asyncio 中,任务可以被取消,这通常通过 CancelledError 体现。

import asyncio


async def worker():
    try:
        print("开始工作")
        await asyncio.sleep(10)
    except asyncio.CancelledError:
        print("收到取消请求")
        raise


async def main():
    task = asyncio.create_task(worker())
    await asyncio.sleep(0.2)
    task.cancel()

    try:
        await task
    except asyncio.CancelledError:
        print("task 已取消")


asyncio.run(main())

这里的重点不是“取消 API 怎么用”,而是理解其与 await 的关系:

这说明 await 协议并不只承载“最终成功值”,也承载“失败与取消”的控制流。

十四、await 与 yield from 的关系

如果想真正理解 await 协议,必须知道它和 yield from 的历史关系。

在 async / await 语法出现前,Python 曾使用“基于生成器的协程”。当时异步协作常通过 yield from 来表达。

现代 Python 里,await 可以理解为对异步委托的专门化语法,它比 yield from 更严格、更清晰,原因包括:

  1. await 只能作用于 awaitable,而不是任意可迭代对象
  2. async def 明确标记了协程函数,避免与普通生成器混淆
  3. 语义上更聚焦于异步等待,而不是一般性的迭代委托

所以,await 不是凭空创造的新机制,而是在生成器语义基础上为异步编程建立的专用协议层。

十五、一个更接近“协议本体”的最小示例

下面这个例子能帮助你从“语言糖”退回到“协议视角”。

import asyncio


class OneShotValue:
    def __init__(self, value):
        self.value = value

    def __await__(self):
        if False:
            yield
        return self.value


async def main():
    result = await OneShotValue(123)
    print(result)


asyncio.run(main())

这个例子看起来有些奇怪,尤其是:

if False:
    yield

它的作用是让这个函数在语法上成为生成器,从而返回一个迭代器对象。虽然不会真的 yield 出什么,但协议上它已经满足“返回迭代器”的要求了,因此 await 能工作。

这个例子很好地说明了一件事:

await 关心的不是“这个对象是不是某个具体类”,而是“它能否通过迭代器协议表达挂起/完成语义”。

不过,这种写法更适合教学,不适合生产环境。工程里一般应当复用已有 awaitable,而不是手写这种技巧性实现。

十六、什么不属于 await 协议

有几个概念很容易混淆,必须分开。

1. 异步迭代不是 await 协议本身

异步迭代使用的是:

对应语法是 async for,而不是 await。

2. 异步上下文管理不是 await 协议本身

异步上下文管理使用的是:

对应语法是 async with,而不是 await。

3. 普通可迭代对象不等于 awaitable

一个对象能被 for 遍历,不代表它能被 await。await 要求的是 awaitable 协议,不是 iterable 协议。

例如列表、普通生成器都不能直接 await。

十七、工程上最常见的几个误区

1. 误以为调用 async def 就已经执行了

错误理解:

“我调用了异步函数,所以它开始运行了。”

实际上,单纯调用 async def 函数只会得到协程对象,不保证执行。执行通常要通过以下方式之一触发:

2. 误以为 await 会阻塞整个线程

不准确。await 会挂起当前协程,但只要底层等待对象是非阻塞式的,事件循环仍然可以在同一线程调度其他任务。

3. 在自定义await里返回了错误类型

最典型错误就是返回普通值、列表、协程函数本身,而不是迭代器。

4. 把“能调用”误认为“能 await”

可调用对象和可等待对象是两个不同维度。

十八、如何从框架角度理解 await 协议

如果你站在框架作者的角度看,await 协议的价值非常大,因为它提供了一层统一抽象:

不管右边是:

只要符合协议,调用方都可以统一写成:

result = await obj

这意味着上层业务逻辑不需要关心底层异步对象的具体类型,只需要关心它是否满足 awaitable 语义。

这也是为什么“协议”比“类继承”更重要。Python 在这里采用的是典型的鸭子类型哲学:

你不必继承某个统一基类,只要行为符合协议即可。

十九、一个完整示例:自定义 awaitable、Task、异常传播放在一起

import asyncio
import inspect


class ResourceLoader:
    def __init__(self, name, delay):
        self.name = name
        self.delay = delay

    def __await__(self):
        async def _load():
            await asyncio.sleep(self.delay)
            if self.name == "bad":
                raise RuntimeError("resource load failed")
            return {"resource": self.name}

        return _load().__await__()


async def consume(obj):
    if not inspect.isawaitable(obj):
        raise TypeError("对象不可等待")
    return await obj


async def main():
    task = asyncio.create_task(consume(ResourceLoader("user", 0.5)))
    result = await task
    print(result)

    try:
        await consume(ResourceLoader("bad", 0.1))
    except RuntimeError as exc:
        print("异常传播成功:", exc)


asyncio.run(main())

这段代码展示了四件事:

  1. 自定义对象可以通过 await 变成 awaitable
  2. inspect.isawaitable 可以做统一判定
  3. Task 可以包装异步工作并被 await
  4. 异常会从底层 awaitable 传播到外层 await 表达式

二十、可以把 await 协议总结成什么

可以把整个 await 协议总结为下面四条:

  1. await 作用的对象必须是 awaitable。
  2. 自定义 awaitable 的关键是实现 await
  3. await 的返回值必须是迭代器,而不是任意对象。
  4. await 的执行语义是:挂起当前协程,等待目标对象完成,然后返回结果或抛出异常。

如果再进一步压缩成一句更偏底层的话:

await 协议就是“用迭代器描述异步挂起点,并由事件循环负责恢复执行”的一套语言约定。

二十一、学习这部分时最值得建立的心智模型

建议你把 Python 异步系统拆成三层看:

  1. 语法层
    async def、await、async for、async with

  2. 协议层
    awaitaiteranextaenteraexit

  3. 调度层
    asyncio event loop、Future、Task、回调恢复

其中,await 协议位于“语法层”和“调度层”之间。它既不是纯语法糖,也不是完整调度器;它是把“可挂起对象”接入异步执行体系的桥。

这一层真正理解了,很多问题都会自然变得清楚:

到此这篇关于Python之Await 协议的实现的文章就介绍到这了,更多相关Python Await 协议内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文