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 关键字的函数

>>> def greeting(name):
... return 'Hello ' + name
...
>>> greeting('Blog')
Hello Blog

>>> def greeting ( name ) :

. . . return 'Hello ' + name

. . .

>>> greeting ( 'Blog' )

Hello Blog

调用后,函数立即执行

对于一个普通的没有 async 关键字的函数,调用它直接得到它的返回值,如果给它加上 async 关键字会怎么样呢?

>>> async def greeting(name):
...     print('called gretting function')
...     return 'Hello ' + name
...
>>> greeting('Blog')
<coroutine object greeting at 0x10dc8ece0>

>>> async def greeting ( name ) :

. . .      print ( 'called gretting function' )

. . .      return 'Hello ' + name

. . .

>>> greeting ( 'Blog' )

< coroutine object greeting at 0x10dc8ece0 >

得到的是一个 coroutine 对象,函数没有立即执行。它有点像是其他语言的 Promise 或 Future,下一步需要对兑现它,在 Python 中要去兑现一个 coroutine 可调用它的 send() 方法

>>> 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

>>> 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 函数

>>> def run(coro):
...     try:
...         coro.send(None)
...     except StopIteration as e:
...         return e.value
...
...
>>> run(greeting('Blog'))
called gretting function
'Hello Blog'

>>> def run ( coro ) :

. . .      try :

. . .          coro . send ( None )

. . .      except StopIteration as e :

. . .          return e . value

. . .

. . .

>>> run ( greeting ( 'Blog' ) )

called gretting function

'Hello Blog'

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

  1. def foo()
  2. async def foo()
>>> 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

>>> 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。

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

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

如果一个普通函数调用 async 函数,显然只会得到一个 coroutine 对象

>>> 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

>>> 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 函数会怎么样呢?

>>> 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

>>> 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  对象。

>>> async def main():
...     print(await greeting('Blog'))
...
...
>>> run(main())
Hello Blog

>>> async def main ( ) :

. . .      print ( await greeting ( 'Blog' ) )

. . .

. . .

>>> run ( main ( ) )

Hello Blog

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

什么时候不能用 await 呢?

在 Python REPL 中对 async 函数不能用 await

>>> await main()
  File "<bpython-input-33>", line 1
SyntaxError: 'await' outside function

>>> await main ( )

   File "<bpython-input-33>" , line 1

SyntaxError : 'await' outside function

普通函数对 async 函数的调用不能 await

>>> def foo():
...     await greeting('Blog')
  File "<bpython-input-35>", line 2
SyntaxError: 'await' outside async function

>>> def foo ( ) :

. . .      await greeting ( 'Blog' )

   File "<bpython-input-35>" , line 2

SyntaxError : 'await' outside async function

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

对一个普通函数用 await 语法上不报错,但执行会有问题

>>> 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

>>> 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

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

 如果对Python有兴趣,想了解更多的Python以及AIoT知识,解决测试问题,以及入门指导,帮你解决学习Python中遇到的困惑,我们这里有技术高手。如果你正在找工作或者刚刚学校出来,又或者已经工作但是经常觉得难点很多,觉得自己Python方面学的不够精想要继续学习的,想转行怕学不会的, 都可以加入我们,可领取最新Python大厂面试资料和Python爬虫、人工智能、学习资料!VX【pydby01】暗号CSDN

asyncio 调用 async 函数


版权声明:本文为pydby01原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。