关于 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 2 3 4 5 |
>>> def greeting(name): ... return 'Hello ' + name ... >>> greeting('Blog') Hello Blog |
调用后,函数立即执行
对于一个普通的没有 async 关键字的函数,调用它直接得到它的返回值,如果给它加上 async
关键字会怎么样呢?
1 2 3 4 5 6 |
>>> async def greeting(name): ... print('called gretting function') ... return 'Hello ' + name ... >>> greeting('Blog') <coroutine object greeting at 0x10dc8ece0> |
得到的是一个 coroutine 对象,函数没有立即执行。它有点像是其他语言的 Promise 或 Future,下一步需要对兑现它,在 Python 中要去兑现一个 coroutine 可调用它的 send() 方法
1 2 3 4 5 6 7 |
>>> g = greeting('Blog') >>> g.send(None) called gretting function Traceback (most recent call last): File "<input>", line 1, in <module> g.send(None) StopIteration: Hello Blog |
coroutine.send(None) 触发了它的执行,同时注意到函数抛出了一个异常 StopIteration(Exception), 该异常的 value 就是 async 函数的返回值,所以也就可以定义一个 run()
函数来执行 async 函数
1 2 3 4 5 6 7 8 9 10 |
>>> def run(coro): ... try: ... coro.send(None) ... except StopIteration as e: ... return e.value ... ... >>> run(greeting('Blog')) called gretting function 'Hello Blog' |
这就要问,加没有 async
有什么区别呢?我们先从字节码来对比以下两个方法的不同
- def foo()
- async def foo()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
>>> def foo(): ... pass ... >>> dis.dis(foo) 2 0 LOAD_CONST 0 (None) 2 RETURN_VALUE >>> async def foo(): ... pass ... >>> dis.dis(foo) 0 GEN_START 1 2 2 LOAD_CONST 0 (None) 4 RETURN_VALUE |
唯一的不同是加了 async
关键字的函数第一条指令是 GEN_START
,开始一个 Generator。
如果不想声明这个 run() 函数,也可以直接使用 asyncio 提供的 run 函数
1 2 3 |
>>> import asyncio >>> asyncio.run(greeting('Blog')) 'Hello Blog' |
asyncio.run() 实际是用 event_loop() 来执行 coroutine 的。
await, async 函数调用另一个 async 函数
明白了 async
函数返回的是一个 coroutine 对象后,这就能指导我们如何去调一个 async 函数,特别是由一个 async 函数调用另一个 async 函数
如果一个普通函数调用 async 函数,显然只会得到一个 coroutine 对象
1 2 3 4 5 6 7 8 9 10 11 12 |
>>> async def greeting(name): ... return 'Hello ' + name ... >>> def main(): ... print(greeting('Blog')) ... ... >>> main() <coroutine object greeting at 0x10ce37df0> <bpython-input-9>:2: RuntimeWarning: coroutine 'greeting' was never awaited print(greeting('Blog')) RuntimeWarning: Enable tracemalloc to get the object allocation traceback |
并且 Python 解释器会发出警告说 coroutine was never awaited, 因为如果没有 await 的话,对 greeting('Blog') 的调用永远得不到执行,也就失去的调用的必要性。
如果由一个 async 函数按传统方式来调用另一个 async 函数会怎么样呢?
1 2 3 4 5 6 7 8 9 10 11 12 |
>>> async def main(): ... print(greeting('Blog')) ... ... >>> main() <coroutine object main at 0x10ce37840> >>> >>> run(main()) <coroutine object greeting at 0x10cf58120> <bpython-input-14>:2: RuntimeWarning: coroutine 'greeting' was never awaited print(greeting('Blog')) RuntimeWarning: Enable tracemalloc to get the object allocation traceback |
没什么意外,main()
返回一个 coroutine, 其他什么也没发生。如果试图去兑现 main() 调用,相当于是普通函数调用了一个 async 函数。
这时就引出 await
关键字了,当一个 async 函数中调用另一个 async 函数时,必须在调用前加上 await 关键字,除非你就想得到一个 coroutine 对象。
1 2 3 4 5 6 |
>>> async def main(): ... print(await greeting('Blog')) ... ... >>> run(main()) Hello Blog |
总结起来就是:由 async 函数发起的对其他 async 函数的调用时,都必须加上 await 关键时。这里 greeting() 也是一个 async 函数,如果它又调用其他的 async 函数,需应用同样的规则,相当于一个 await 链。当对入口的 async 函数发起执行(兑现)时,将会发现链式反应。
什么时候不能用 await 呢?
在 Python REPL 中对 async 函数不能用 await
1 2 3 |
>>> await main() File "<bpython-input-33>", line 1 SyntaxError: 'await' outside function |
普通函数对 async 函数的调用不能 await
1 2 3 4 |
>>> def foo(): ... await greeting('Blog') File "<bpython-input-35>", line 2 SyntaxError: 'await' outside async function |
上面那两种是一样的情况,因 Python REPL 就是用一个普通函数作为入口的
对一个普通函数用 await 语法上不报错,但执行会有问题
1 2 3 4 5 6 7 8 9 10 11 12 13 |
>>> async def foo(): ... print(await len('abc')) ... ... >>> run(foo()) Traceback (most recent call last): File "<input>", line 1, in <module> run(foo()) File "<input>", line 3, in run coro.send(None) File "<input>", line 2, in foo print(await len('abc')) TypeError: object int can't be used in 'await' expression |
能不用用 await
- 针对 coroutine 使用 await, 也就是调用 async 修饰的函数用 await
- 发起调用的函数必须为一个 async 函数,或 coroutine.send(), 或者 asyncio 的 event loop
- 随着 Python 版本的演进,更多的地方可以用 await,只要跟着感觉走,哪里不能用 await 错了次就知道了
asyncio 调用 async 函数
1 2 3 4 5 6 7 8 |
>>> async def greeting(name): ... return 'Hello ' + name ... >>> import asyncio >>> event_loop = asyncio.new_event_loop() >>> >>> event_loop.run_until_complete(greeting('Blog')) 'Hello Blog' |
链接:
本文链接 https://yanbin.blog/python-async-await-keywords/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。