Python async, await 的理解与使用

关于 Python 中 async, await 关键字的一些知识在去年的一篇 探索 Flask 对 asyncio 的支持 有讲到,一直没在实际上用过它们,所以基本上也就忘干净了。随着 Flask 2 加入了 async 的特性,以及 FastAPI 从一开始支持 async, 又觉得有必要重新温习一下 Python 中如何使用 async, await 关键字了。

注:由于 Flask 支持了 async, 号称 async 化 Flask 的  Quart 项目开始变得无足轻重了。

本文主要的学习材料是在 YouTube 上的一个视频 Fear and Awaiting in Async (Screencast), 其中用 Python REPL 以 Live 的形式展示,对 async, await 关键字循序渐进的讲解。

如今不少语言都支持 async, await 关键字,如 C#, JavaScript, Kotlin, Rust 等,还有今天的主角 Python。而 Java 仍然很重视函数返回值的意义,未支持 async, 只能显式的返回 Future/CompletableFuture, 而且自己控制如何在线程池中执行。

这又来到了一个叫做纤程的概念,它隐藏在 Python 的 async, await 背后,Go 语言对纤程的支持就更简单了, go foo() 就是了。

async 关键字的函数

1>>> def greeting(name):
2... return 'Hello ' + name
3...
4>>> greeting('Blog')
5Hello Blog

调用后,函数立即执行

对于一个普通的没有 async 关键字的函数,调用它直接得到它的返回值,如果给它加上 async 关键字会怎么样呢?
1>>> async def greeting(name):
2...     print('called gretting function')
3...     return 'Hello ' + name
4...
5>>> greeting('Blog')
6<coroutine object greeting at 0x10dc8ece0>

得到的是一个 coroutine 对象,函数没有立即执行。它有点像是其他语言的 Promise 或 Future,下一步需要对兑现它,在 Python 中要去兑现一个 coroutine 可调用它的 send() 方法
1>>> g = greeting('Blog')
2>>> g.send(None)
3called gretting function
4Traceback (most recent call last):
5  File "<input>", line 1, in <module>
6    g.send(None)
7StopIteration: Hello Blog

coroutine.send(None) 触发了它的执行,同时注意到函数抛出了一个异常 StopIteration(Exception), 该异常的 value 就是 async 函数的返回值,所以也就可以定义一个 run() 函数来执行 async 函数
 1>>> def run(coro):
 2...     try:
 3...         coro.send(None)
 4...     except StopIteration as e:
 5...         return e.value
 6...
 7...
 8>>> run(greeting('Blog'))
 9called gretting function
10'Hello Blog'

这就要问,加没有 async 有什么区别呢?我们先从字节码来对比以下两个方法的不同

  1. def foo()
  2. async def foo()
 1>>> def foo():
 2...     pass
 3...
 4>>> dis.dis(foo)
 5  2           0 LOAD_CONST               0 (None)
 6              2 RETURN_VALUE
 7>>> async def foo():
 8...     pass
 9...
10>>> dis.dis(foo)
11              0 GEN_START                1
12
13  2           2 LOAD_CONST               0 (None)
14              4 RETURN_VALUE

唯一的不同是加了 async 关键字的函数第一条指令是 GEN_START,开始一个 Generator。

如果不想声明这个 run() 函数,也可以直接使用  asyncio 提供的  run 函数
1>>> import asyncio
2>>> asyncio.run(greeting('Blog'))
3'Hello Blog'

asyncio.run() 实际是用 event_loop() 来执行 coroutine 的。

await, async 函数调用另一个 async 函数

明白了 async 函数返回的是一个  coroutine 对象后,这就能指导我们如何去调一个 async 函数,特别是由一个 async 函数调用另一个 async 函数

如果一个普通函数调用 async 函数,显然只会得到一个 coroutine 对象
 1>>> async def greeting(name):
 2...     return 'Hello ' + name
 3...
 4>>> def main():
 5...     print(greeting('Blog'))
 6...
 7...
 8>>> main()
 9<coroutine object greeting at 0x10ce37df0>
10<bpython-input-9>:2: RuntimeWarning: coroutine 'greeting' was never awaited
11  print(greeting('Blog'))
12RuntimeWarning: Enable tracemalloc to get the object allocation traceback

并且 Python 解释器会发出警告说 coroutine was never awaited, 因为如果没有 await 的话,对 greeting('Blog') 的调用永远得不到执行,也就失去的调用的必要性。

如果由一个 async 函数按传统方式来调用另一个 async 函数会怎么样呢?
 1>>> async def main():
 2...     print(greeting('Blog'))
 3...
 4...
 5>>> main()
 6<coroutine object main at 0x10ce37840>
 7>>>
 8>>> run(main())
 9<coroutine object greeting at 0x10cf58120>
10<bpython-input-14>:2: RuntimeWarning: coroutine 'greeting' was never awaited
11  print(greeting('Blog'))
12RuntimeWarning: Enable tracemalloc to get the object allocation traceback

没什么意外,main() 返回一个 coroutine, 其他什么也没发生。如果试图去兑现 main() 调用,相当于是普通函数调用了一个  async 函数。

这时就引出 await 关键字了,当一个 async 函数中调用另一个 async 函数时,必须在调用前加上 await 关键字,除非你就想得到一个 coroutine  对象。
1>>> async def main():
2...     print(await greeting('Blog'))
3...
4...
5>>> run(main())
6Hello Blog

总结起来就是:由 async 函数发起的对其他 async 函数的调用时,都必须加上 await 关键时。这里 greeting() 也是一个 async 函数,如果它又调用其他的 async 函数,需应用同样的规则,相当于一个 await 链。当对入口的 async 函数发起执行(兑现)时,将会发现链式反应。

什么时候不能用 await 呢?

在 Python REPL 中对 async 函数不能用 await
1>>> await main()
2  File "<bpython-input-33>", line 1
3SyntaxError: 'await' outside function

普通函数对 async 函数的调用不能 await
1>>> def foo():
2...     await greeting('Blog')
3  File "<bpython-input-35>", line 2
4SyntaxError: 'await' outside async function

上面那两种是一样的情况,因 Python REPL 就是用一个普通函数作为入口的

对一个普通函数用 await 语法上不报错,但执行会有问题
 1>>> async def foo():
 2...     print(await len('abc'))
 3...
 4...
 5>>> run(foo())
 6Traceback (most recent call last):
 7  File "<input>", line 1, in <module>
 8    run(foo())
 9  File "<input>", line 3, in run
10    coro.send(None)
11  File "<input>", line 2, in foo
12    print(await len('abc'))
13TypeError: object int can't be used in 'await' expression

能不用用 await

  1. 针对 coroutine 使用 await, 也就是调用 async 修饰的函数用 await
  2. 发起调用的函数必须为一个 async 函数,或 coroutine.send(), 或者 asyncio 的 event loop
  3. 随着 Python 版本的演进,更多的地方可以用 await,只要跟着感觉走,哪里不能用 await 错了次就知道了

asyncio 调用 async 函数

1>>> async def greeting(name):
2...     return 'Hello ' + name
3...
4>>> import asyncio
5>>> event_loop = asyncio.new_event_loop()
6>>>
7>>> event_loop.run_until_complete(greeting('Blog'))
8'Hello Blog'

链接:
  1. Fear and Awaiting in Async (Screencast)
永久链接 https://yanbin.blog/python-async-await-keywords/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。