为什么引入 async/await
关于 async/await 最初的提案,参见:PEP492 以及它的中文翻译。
具体细节我们先不做了解,先看一下增加 async/await 解决了什么问题(引自翻译):
在以前,我们可以用生成器实现协程(PEP 342),后来又对其进行了改进,引入了 yield from
语法(PEP 380)。但仍有一些缺点:
- 协程和普通生成器使用相同的语法,所以很容易把它们搞混,初学者更是如此。
- 一个函数是否是一个协程,取决于它里面是否出现了
yield
或yield from
语句。这并不明显,容易在重构函数的时候搞乱,导致出错。 - 异步调用被
yield
语法限制了,我们不能获得、使用更多的语法特性,比如with
和for
。
PEP492 把协程从生成器独立出来,成为 Python 的一个原生事物。这会消除协程和生成器之间的混淆,方便编写不依赖特定库的协程代码。也为 linter 和 IDE 进行代码静态分析提供了机会。
使用原生协程和相应的新语法,我们可以在异步编程时使用上下文管理器(context manager)和迭代器。如下文所示,新的 async with 语句可以在进入、离开运行上下文(runtime context)时进行异步调用,而 async for 语句可以在迭代时进行异步调用。
综上,虽然在 CPython 的内部实现,用 def async
定义的协程仍然是一个生成器,但是已经被封装好了,我们不用关心其晦涩的细节。添加 async/await 后我们编写异步协程程序会更加的容易、直观。同时增加异步迭代器 async for
和异步上下文管理器 async with
,我们在编写异步程序将更加的方便。
详细内容
一般我们将新语法 async/await 定义的协程称为:原生协程;将传统方法实现的协程称为:生成器协程。
语法
使用如下语法声明原生协程:
1 | async def read_data(db): |
协程语法有如下关键点:
async def
函数必定是协程,即使里面不含有await
语句。- 如果在 async 函数里面使用
yield
或yield from
语句,会引发SyntaxError
异常。 - 在 CPython 内部,引入两个新的代码对象标识(code object flags):
CO_COROUTINE
表示这是原生协程。(由新语法定义)CO_ITERABLE_COROUTINE
表示这是用生成器实现的协程,但是和原生协程兼容。(用装饰器types.coroutine()
装饰过的生成器协程)
- 调用一个普通生成器,返回一个生成器对象(generator object);相应的,调用一个协程返回一个协程对象(coroutine object)。
- 协程不再抛出
StopIteration
异常,作为替代会抛出RuntimeError
异常。 - 如果一个协程从未等待就被垃圾收集器销毁了,会引发一个
RuntimeWarning
异常。
types.coroutine()
types 模块添加了一个新函数 coroutine(fn)
。“用生成器实现的协程”和“原生协程”本身是不互通,不兼容的,使用它可以让两者之间的互操作。
1 |
|
await 表达式
新的 await 表达式用于获得协程执行的结果:
1 | async def read_data(db): |
await
和 yield from
类似,它挂起 read_data
的执行,直到 db.fetch
执行完毕并返回结果。
以 CPython 内部,await
使用了 yield from
的实现,但加入了一个额外步骤——验证它的参数类型。await
只接受 awaitable
对象,awaitable
对象是以下的其中一个: