WeeklyPEP-8-PEP 492-使用 async 和 await 语法的协程-overview

2024-03-06 08:42:35 浏览数 (2)

前言

本文的主体内容大部分来自对 PEP 492 原文的翻译,剩余部分是本人对原文的理解,在整理过程中我没有刻意地区分二者,这两部分被糅杂在一起形成了本文。因此请不要带着「本文的内容是百分百正确」的想法阅读。如果文中的某些内容让你产生疑惑,你可以给我留言与我讨论或者对比 PEP 492 的原文加以确认。

注:PEP 492 创建于 2015-04-09,Python 3.5

注:文中的「当前版本」指的是本提案生效之前的版本

注:本文过长,虽然已经自我校对过一次,但还是难免存在错别字或语句不通顺的地方,如果您发现了问题欢迎留言给我

摘要

网络请求爆发性地增长引发了对低延时、可拓展代码的相关需求。本提案旨在让显式地编写异步、并发 Python 代码更容易、更 Pythoinc,并以此满足前述需求。

提案建议使协程成为 Python 中完全独立的新概念,并引入新的支持语法。最终的目的是在 Python 中建立一个简洁通用的异步编程心智模型,并使它尽可能接近同步编程

在本提案中,假设异步任务都使用类似内置模块 asyncio.events.AbstractEventLoop 中的事件循环进行编排和协调。但是,本提案与任何特定的事件循环实现无关,只与使用 yield 作为调度信号的协程相关,也就是说协程会在事件(例如 IO)完成前保持等待。

我们相信,本提案能够让 Python 在快速增长的异步编程领域中继续保持竞争力,因为很多其他语言已经或计划采用近似的特性:2,5,7,8,10。

接口设计与实施修订

注:这部分是修订内容,可以放在最后阅读。

  1. 以初始 Python 3.5 beta 版本的反馈为依据,重构本提案设置的对象模型。这次重构的目的是更明确地将原生协程与生成器分离,而不是将原生协程作为一种新的生成器,原生协程要设计成完全独立的类型(具体实施在 引用 17)。这么做的主要原因是在尝试为 Tornado Web Server 集成原生协程时遇到了问题(记录在 引用 18)。
  2. CPython 3.5.2 更新了 __aiter__ 协议。在 3.5.2 之前,__aiter__ 返回一个可以被解析成 异步迭代器 的 可等待对象(awaitable)。从 3.5.2 开始,__aiter__ 直接返回异步迭代器。如果在 3.5.2 中使用旧协议,会抛出一个 PendingDeprecatioWarning 异常。而在 3.6 中,旧的 __aiter__ 协议仍旧会被支持,但会抛出一个 DeprecationWarning 异常。最终,在 3.7 中,旧的 __aiter__ 协议将被废弃,如果 __aiter__ 返回的不是异步迭代器,则会引发 RuntimeError。可以通过 引用 19 和 引用 20 来获取更多细节。

理由和目标

当前版本的 Python 支持通过生成器来实现协程(PEP 342),PEP 380 中引入的 yield from 语法进一步增强了这一特性。但是这种方案有很多缺点:

  1. 生成器实现的协程和正常生成器的语法相同,因此很容易被混淆,对于新用户来说尤其如此;
  2. 一个函数是否是协程取决于函数体中是否存在 yieldyield from 语句。在重构这些函数时,如果删除或新增了 yield 相关语句就可能会导致一些不明显的错误;
  3. 只能在 yield 语法支持的地方进行异步调用,无法异步调用类似 with 或 for 这样的语句,限制了可用性。

本提案使协程成为 Python 语言的一种原生特性,并且清晰地将其与生成器区分开。这样做不仅消除了生成器与协程之间的歧义,还可以不依赖特定库直接定义协程。同时也提升了 linters 或 IDE 静态代码分析和重构的能力。

原生协程以及相关新语法使得在异步操作中定义上下文管理器和可迭代协议成为可能。稍后会在提案中提及:新的 async with 语句允许 Python 程序在进入或退出上下文上时执行异步调用,而新的 async for 语句可以在迭代器中执行异步调用。

规范

规范章节引入了新的语法和语义,以增强 Python 对协程的支持。

本规范假定阅读者已经了解此前 Python 中协程的实现( PEP 342 和 PEP 380)。本规范涉及的语法修改动机来自 asyncio 模块提案(PEP 3156)和 Cofunctions 提案(PEP 3152,现已被本规范否决)。

在后文中,将使用「原生协程」来指代使用新语法声明的协程,使用「生成器式协程」指代基于生成器语法的协程。

原生协程声明语法

原生协程声明语法如下:

代码语言:javascript复制
async def read_data(db):
	pass

它的主要特性有:

  1. 使用 async def 声明的函数一定是协程,即使内部不包含 await
  2. async 函数中使用 yield yield from 会引发 SyntaxError 异常;
  3. 在内部,引入了两个新的 code object flags:
    1. CO_COROUTINE:用于标记原生协程;
    2. CO_ITERABLE_COROUTINE:使生成器式协程与原生协程兼容(由 types.coroutine 函数设置)。
  4. 常规生成器返回一个生成器对象,类似的,协程返回一个协程对象
  5. 在协程中 StopIteration 会被 RuntimeError 代替,对于常规生成器来说,这种行为会在后续过程中支持(详情请看 PEP 479);
  6. 如果不使用 await 直接调用原生协程,当它被垃圾回收时会抛出一个 RuntimeWarning(点击 用于调试的特性 了解更多);
  7. 更多特性请看:协程对象 章节。

types.coroutine()

types 模块中新增了一个名为 coroutine(fn) 的函数。它能够帮助「asyncio 中现有的生成器式协程」与「本提案引入的原生协程」实现相互兼容:

代码语言:javascript复制
@types.coroutine
def process_data(db):
	data = yield from read_data(db)
	...
代码语言:javascript复制
# CPython 3.10 Lib.types.py
def coroutine(func):  
    if not callable(func):  
        raise TypeError('types.coroutine() expects a callable')  
  
    if (
	    func.__class__ is FunctionType and 
	    getattr(func, '__code__', None).__class__ is CodeType
	):  
        co_flags = func.__code__.co_flags  
		
		# 0x20 == CO_GENERATOR 生成器标识
		# 0x180 == CO_COROUTINE | CO_ITERABLE_COROUTINE 原生协程标识 or 生成式协程标识
		# 0x100 == CO_ITERABLE_COROUTINE 生成式协程标识

		# 如果确定传入函数是一个原生协程则直接返回
        if co_flags & 0x180:  
            return func  

		# 如果传入函数是一个生成器函数,
		# 则将 CO_ITERABLE_COROUTINE 标记附加在此函数上,随后返回
        if co_flags & 0x20:
            co = func.__code__
            func.__code__ = co.replace(co_flags=co.co_flags | 0x100)
            return func  

	# 下面的代码主要用于兼容「返回类似生成器对象」的函数
	# 例如使用 Cython 编译的生成器
	import functools
    import _collections_abc
    @functools.wraps(func)
    def wrapped(*args, **kwargs):  
        coro = func(*args, **kwargs)  
        if (coro.__class__ is CoroutineType or  
            coro.__class__ is GeneratorType and coro.gi_code.co_flags & 0x100):  
            return coro  
        if (isinstance(coro, _collections_abc.Generator) and  
            not isinstance(coro, _collections_abc.Coroutine)):  
            return _GeneratorWrapper(coro)  
        return coro  
  
    return wrapped

如果 fn 是生成器函数, types.coroutine() 会在它的 code object 中添加 CO_ITERABLE_COROUTINE 标志,使其返回一个协程对象。

如果 fn 不是生成器函数,types.coroutine() 会对齐进行包装。如果 fn 返回一个生成器函数,返回的函数会被 _GeneratorWrapper 包装。

type.coroutine() 不会为生成器函数附加 CO_COROUTINE 标志,以便区分「原生协程」和「生成器式协程」。

await 表达式

await 表达式用来获取一个协程执行的结果:

代码语言:javascript复制
async def read_data(db):
	data = await db.fetch("SELECT ...")
	...

awaityield from 近似,会暂停 read_data 函数的执行直到可等待对象 db.fetch 完成并返回结果。await 使用 yield from 实现,但是多了一个验证参数的步骤。await 后只能跟一个 可等待对象(awaitable),可以是以下选项之一:

  1. 原生协程函数返回的原生协程对象;
  2. types.coroutine() 装饰的函数中返回的生成式协程对象;
  3. 一个拥有 __await__ 方法的对象,且该方法需要返回一个迭代器;
  4. 使用 CPython C API 定义的带有 tp_as_async.am_await 函数的对象,该函数返回一个迭代器(类似 __await__ 方法)。

关于第三点一些延伸内容:任何 yield from 调用链都会以 yield 收尾,这是 Futures 执行的必要条件。由于协程本质上是一个特殊的生成器,因此每个 await 都会被 await 调用链上的某个 yield 挂起(详情请参考 PEP 3156)。为了在协程上实现这种行为,一个名为 __await__ 的新魔术方法被添加进来。例如,在 asyncio 中, 要想在 await 语句中使用 Future,唯一要做的就是在 asyncio.Future 类中添加 __await__ = __iter__。后续章节中,称带有 __await__ 方法的对象为类 Future 对象。如果 __await__ 返回迭代器之外的东西,会抛出 TypeError 异常。

在原生协程外部使用 await 会抛出 SyntaxError 异常(就像在一般函数外调用 yield 一样)。

不在 await 关键字后使用可等待对象会抛出 TypeError 异常。

更新运算符优先级

await 关键字被定义为:

代码语言:javascript复制
power ::= await ["**" u_expr]
await ::= ["await"] primary

其中「primary」代表语言中最主要的操作,其语法为:

代码语言:javascript复制
primary ::= atom | attributerf | subscription | slicing | call

如需理解上述表达式含义可参考 引用 12 和 语法更新。

yieldyield from 不同,大多数情况下 await 表达式不需要被圆括号包裹。此外,yield from 允许将任何表达式作为参数,甚至可以 yield from a() b(),它会被解析为 yield from (a() b())。这看起来很反常识就像一个 BUG 一样,因为一般来说,算数操作不会产生可等待对象。为了避免此类问题在 await 表达式中再次出现,await 的优先级被设定为低于 []**,** ()**,** . 但高于 ``**。

具体优先级如下(从上到下,从低到高):

Operator

Description

yield x, yield from x

Yield 表达式

lambda

Lambda 表达式

if - else

条件语句

or

布尔 或

and

布尔 且

not x

布尔 非

in, not in, is, is not, <, <=, >, >=, !=, ==

比较,包括成员测试和身份测试

|

位运算 或

^

位运算 异或

&

位运算 且

<<, >>

位运算 左移和右移

, -

加减运算

*, @, /, //, %

乘、矩阵乘、除、地板除、取余

x, -x, ~x

正、负、按位取反

**

幂运算

await x

await 表达式

xindex, xindex:index, x(args...), x.attribute

索引、切片、调用、属性

(expressions...), expressions..., {key: value...}, {expressions...}

元组生成器、列表生成器、字典生成器、集合生成器

使用 await 关键字的示例

有效调用:

Expression

Will be parsed as

if await fut: pass

if (await fut): pass

if await fut 1: poass

if (await fut) 1: pass

pair = await fut, 'spam'

pair = (await fut), 'spam'

with await fut, open(): pass

with (await fut), open(): pass

await foo()'spam'.baz()()

await ( foo()'spam'.baz()() )

return await coro()

return ( await coro() )

res = await coro() ** 2

res = (await coro()) ** 2

func(a1=await coro(), a2=0)

func(a1=(await coro()), a2=0)

await foo() await bar()

(await foo()) (await bar())

-await foo()

-(await foo())

无效调用:

Expression

Shoud be written as

await await coro()

await (await coro())

await -coro()

await (-coro())

异步上下文管理器与 async with

注:关于上下文管理器的内容可以参考:WeeklyPEP-2-PEP343-with 语句-overview

所谓异步上下文管理器,是一种能够在进入或退出上下文时调用异步代码的上下文管理器。为了实现它,本规范单独为异步上下文提出了一个新协议,此协议由两个新的魔术方法组成:__aenter____aexit__它们都必须返回一个可等待对象

异步上下文管理器的示例:

代码语言:javascript复制
class AsyncContextManager:
	async def __aenter__(self):
		await log("entering context")

	async def __aexit__(self, exc_type, exc, tb):
		await log("exiting context")
新语法

本规范规定的异步上下文管理器声明方式如下:

代码语言:javascript复制
async with EXPR as VAR:
	BLOCK

语义上等同于:

代码语言:javascript复制
mgr = (EXPR)
aexit = type(mgr).__aexit__
aenter = type(mgr).__aenter__

VAR = await aenter(mgr)
try:
	BLOCK
except:
	if not await aexit(mgr, *sys.exc_info()):
		raise
else:
	await aexit(mgr, None, None, None)

与普通的 with 语句一样,可以在单个 async with 语句中指定多个上下文管理器。

不能将没有实现 __aenter____aexit__ 的普通上下文管理器传递给 async with。在 async def 函数之外使用 async with 会抛出 SyntaxError 异常。

示例

通过异步上下文管理器可以很方便的在协程中实现数据库事务管理器:

代码语言:javascript复制
async def commit(session, data):
	...
	async with session.transaction():
		...
		await session.update(data)
		...

也可以很简洁的使用锁:

代码语言:javascript复制
async with lock:
	...

代替之前的:

代码语言:javascript复制
with (yield from lock):
	...

异步迭代器和 async for

所谓异步迭代器,是一种可以在 iter 和 next 方法中调用异步代码的迭代器。要想实现它:

  1. 指定对象必须实现一个返回异步迭代器对象的 __aiter__ 方法(如果是通过 CPython C API 定义则需要定义 tp_as_async.am_aiter slot 代替 __aiter__);
  2. 异步迭代器对象必须实现一个返回可等待对象的 __anext__ 方法(如果是通过 CPython C API 定义则需要定义 tp_as_async.am_anext slot 代替 __anext__);
  3. 为了使迭代过程不会无限进行下去,__anext__ 必须在适当的时候抛出 StopAsyncIteration 异常。

异步迭代器示例:

代码语言:javascript复制
class AsyncIterable:
	def __aiter__(self):
		return self

	async def __anext__(self):
		data = await self.fetch_data()
		if data:
			return data
		else:
			raise StopAsyncIteration

	async def fetch_data(self):
		...
新语法
代码语言:javascript复制
async for TARGET in ITER:
	BLOCK
else:
	BLOCK2

语义上等同于:

代码语言:javascript复制
iter = (ITER)
iter = type(iter).__aiter__(iter)
running = True

while running:
	try:
		TARGET = await type(iter).__anext__(iter)
	except StopAsyncIteration:
		runnint = False
	else:
		BLOCK
else:
	BLOCK2

async for 后使用未实现 __aiter__ 方法的常规迭可迭代对象会抛出 TypeError 异常,在 async def 外使用 async for 会抛出 SyntaxError 异常。

与常规 for 语句一样,async for 也有一个可选的 else 字句。

示例 1

通过异步迭代器可以在迭代过程中异步缓冲数据:

代码语言:javascript复制
async for data in cursor:
	...

其中,cursor 是一个异步迭代器,每迭代 N 次就会从数据库中预取 N 行数据。

下面的代码实现了异步迭代协议:

代码语言:javascript复制
class Cursor:
	def __init__(self):
		self.buffer = collections.deque()

	async def _prefetch(self):
		...

	def __aiter__(self):
		return self

	async def __anext__(self):
		if not self.buffer:
			self.buffer = await self._prefetch()
			if not self.buffer:
				raise StopAsyncIteration
		return self.buffer.popleft()

然后 Cursor 类可以像这样被使用:

代码语言:javascript复制
async for row in Cursor():
	print(row)

等同于下面这段代码:

代码语言:javascript复制
i = Cursor().__aiter__()
while True:
	try:
		row = await i.__anext__()
	except StopAsyncIteration:
		break:
	else:
		print(row)
示例 2

下面的示例是一个通用的工具类,它能够将常规迭代器转换为异步迭代器。虽然这不是一个会经常使用的操作,但是这个示例代码说明了常规迭代器和异步迭代器之间的关系:

代码语言:javascript复制
class AsyncIteratorWrapper:
	def __init__(self, obj):
		self._it = iter(obj)

	def __aiter__(self):
		return self

	async def __anext__(self):
		try:
			value = next(self._it)
		expect StopIteration:
			raise StopAsyncIteration
		return value

async for letter in AsyncIteratorWrapper("abc"):
	print(letter)
为什么需要 StopAsyncIteration

为什么需要 StopAsyncIteration 也就是为什么不继续使用 StopIteration。协程的本质是生成器,所以在 PEP 479 之前,下面的两段代码没有本质上的不同:

代码语言:javascript复制
def g1():
	yield from fut
	return "spam"

代码语言:javascript复制
def g2():
	yield from fut
	raise StopIteration("spam")

由于 PEP 479 被接受并且在协程中默认启用,下面的示例代码将会使用 RuntimeError 包裹 StopIteration

代码语言:javascript复制
async def a1():
	await fut
	raise StopIteration("spam")

因此通知外部代码迭代结束的唯一方案就是抛出一个 StopIteration 以外的异常,也正是因为这样才需要新增一个内置的 StopAsyncIteration 异常。此外,根据 PEP 479 中的定义,所有在协程中抛出的 StopIteration 异常都会被封装在 RuntimeError

协程对象

与生成器的不同之处

本小节仅适用于带有 CO_COROUTINE 的原生协程,即通过 async def 语法定义的协程。asyncio 中现有的生成器式协程的行为保持不变。

为了确保协程与生成器作为不同的概念处理需要付出很大的努力:

  1. 原生协程对象没有实现 __iter____next__ 方法。因此,它不能通过 iter()list()tuple() 或其他内置方法迭代,同样不能在 for .. in 中使用。若要强行在原生协程中实现 __iter____next__ 会抛出 TypeError 异常;
  2. 不能使用 yield from 加原生协程返回正常的生成器,这个行为会抛出 TypeError 异常;
  3. 可以使用 yield from 加原生协程返回生成器式协程(在 asyncio 代码中必须使用 @asyncio.coroutine);
  4. inspect.isgenerator()inspect.isgeneratorfunction() 在接收原生协对象和原生协程方法时需要返回 False
协程对象的内置方法

在底层实现上,协程继承自生成器共享实现代码。所以,协程类似生成器拥有 throw()send()close() 方法,StopIterationGeneratorExit 在协程中也起相同的作用(尽管 PEP 479 默认在协程中启用)。协程的 throw()send() 方法被用来将值或异常传递给类 Future 对象。

更多细节请看 PEP 342,PEP 380 和 Python 文档相关章节。

用于调试的特性

注:asyncio.coroutine 在 Python 3.8 之后被标记为废弃,并在 Python 3.11 正式删除。

注:被标记为废弃的是 asyncio.coroutine 而不是 types.coroutine

注:这一小节的内容我看完之后有点犯迷糊,不知道他在表述什么事情。

新手容易犯的一个错误是忘记可以在协程中使用 yield from

代码语言:javascript复制
@asyncio.coroutine
def useful():
	# 如果没有 yield from 语句,这段代码将不会起作用
	asyncio.sleep(1)

为了调试这类错误,asyncio 中有一种特殊的调试模式,其中 @coroutine 装饰器使用一个特殊的对象包装所有传递进来的函数,这个对象的析构函数会记录警告日志。每当被包装的生成器被 GC 进行垃圾回收时,就会产生一条详细的日志信息,其中包含该装饰器确切的定义位置、被回收位置的堆栈跟踪等信息。封装对象还提供了一个方便的 __repr__ 函数,一种包含有关生成器的详细信息。

问题是如何启动这些调试功能。调试功能在生产环境下应该是不可用的,所以 @coroutine 装饰器根据操作系统环境变量 PYTHONSYNCIODEBUG 来判断是否起作用。这样就可以在运行 asyncio 程序时使用 asyncio 自带的函数。EventLoop.set_debug(一种不用的调试工具)对 @coroutine 装饰器的行为没有影响。

为了使协程就成为与生成器不同的原生概念:

  1. 如果协程未被 await 直接调用会抛出 RuntimeWarning 异常;
  2. 还建议在 sys 模块中添加两个新函数:set_coroutine_wrapperget_coroutine_wrapper。它们的作用是在 asyncio 或其他框架中启用高级调试功能(例如显示创建协程的具体位置,以及更详细的垃圾回收堆栈跟踪)。

新内置函数

  1. types.coroutine(gen):点击 types.coroutine()(https://wiki.blanc.site/#types.coroutine()) 了解更多;
  2. inspect.iscoroutine(obj):如果 obj 是原生协程对象,返回 True
  3. inspect.iscoroutinefunction(obj):如果 obj 是原生协程函数,返回 Ture
  4. inspect.isawaitable(obj):如果 obj 是可等待对象,返回 True
  5. inspect.getcoroutinestate(coro):返回原生协程对象的当前状态(inspect.getfgeneratorstate(gen) 的逆向函数);
  6. inspect.getfgeneratorstate(gen):返回本地协程独享的局部变量与其值的映射(inspect.getcoroutinestate(coro) 的逆向函数);
  7. sys.set_coroutine_wrapper(wrapper):允许拦截原生协程的创建,在原生协程创建时调用 wrapperwrapper 可以是「一个接受一个参数(一个协程对象)的可调用对象」或是 None。如果是 None 则会重置之前定义的 wrapper,如果调用多次,新的 wrpaaer 将取代之前的。该函数是线程绑定的;
  8. sys.get_coroutine_wrapper():返回通过 sys.set_coroutine_wrapper 设置的 wrapper,如果没设置则返回 None。该函数是线程绑定的。

新的抽象基类

为了更好的与现有框架(如 Tornado,参考 引用 13)和编译器(如 Cython,参考 引用 16)集成,新增了两个抽象基类:

  1. collections.abc.Awaitable:为类 Future 对象创建的基类,实现了 __await__ 方法;
  2. collection.abc.Coroutine:为协程对象创建的基类,实现了 send(value)throw(type, exc, tb)close__await__() 方法。

注意,带有 CO_ITERABLE_COROUTINE 标志的生成器式协程没有实现 __await__ 方法,因此不是 collections.abc.Coroutinecollections.abc.Awaitable 基类的实例:

代码语言:javascript复制
@types.coroutine
def gencoro():
	yield

assert not isinstance(gencoro(), collections.abc.Coroutine)

# 应该如何识别:
assert inspect.isawaitable(gencoro())

为了能更简单地测试指定对象是否支持异步迭代,又引入了另外两个基类:

  1. collections.abc.AsyncIterable:测试是否存在 __aiter__ 方法;
  2. collections.abs.AsyncIterator:测试是否存在 __aiter____anext__ 方法。

术语表

原生协程函数

Navite coroutine function,通过 async def 定义的协程函数,点击 原生协程声明语法 了解更多。

原生协程

Navite coroutine,从原生协程函数返回的内容,点击 await 表达式 了解更多。

生成器式协程函数

Generator-based coroutine function,基于生成器语法的协程,更常见的示例是使用 @asyncio.coroutine 定义的函数。

生成器式协程

Generator-based coroutine,通过生成器式协程函数返回的内容。

协程

Coroutine,原生协程或生成器式协程。

协程对象

Coroutine object,原生协程对象或生成器式协程对象。

类 Future 对象

Future-like object,拥有 __await__ 方法的对象或拥有 tp_as_async->am_await 函数的 C Object,且该函数或方法返回一个迭代器。可以在协程中作为 await 表达式的参数。在协程中 await 类 Future 对象时,协程会被推迟直到类 Future 对象的 __await__ 完成并且返回结果,点击 await 表达式 了解更多。

可等待对象

Awaitable,类 Future 对象或协程对象。点击 await 表达式 了解更多。

异步上下文管理器

Asynchronous context manager,拥有 __aenter____aexit__ 方法的对象,可以搭配 async with 使用,点击 异步上下文管理器与 async with 了解更多。

异步可迭代对象

Asynchronous iterable,拥有 __aiter__ 方法的对象,该方法返回一个异步迭代器对象。可以搭配 async for 一起使用,点击 异步迭代器和 async for 了解更多。

异步迭代器

Asynchronos iterator,拥有 __anext__ 方法的对象,点击 异步迭代器和 async for 了解更多。

过渡计划

tokenizer.c 文件是 CPython 源码中的一个文件,主要负责实现 Python 解释器中的词法分析器。

为了解决 asyncawait 的向后兼容性问题,需要对 tokenizer.c 进行如下修改:

  1. 识别 async def NAME 标记组合;
  2. 在对 async def 块进行词法分析时,会将 async NAME 标记替换为 ASYNC,将 await NAME 标记替换为 AWAIT
  3. 在对 def 块进行词法分析时,会保持 asyncawait NAME 不变。

这种实现方式能够让新语法(只能在 async 函数中使用)与现有代码无缝结合。一个既包含 async def 又包含 async 属性的示例:

代码语言:javascript复制
class Spam:
	async = 42

# 协程函数能够被执行并且打印 42
async def ham():
	print(getattr(Spam, "async"))

向后兼容性

为了兼容新语法,需要确保在现有的内置模块中不存在与 asyncawait 关键字冲突的命名,且新的原生协程需要兼容之前存在的生成器式协程。

asyncio

注:在本 PEP 实施之前,asyncio 库中已经存在了一个名为 async 的函数。

asyncio 模块进行了调整和测试,使现有协程方案与新语法保持兼容,保证 100% 向后兼容,即现有代码能够在新版本中正常运行。

进行调整的主要有:

  1. 使 @asyncio.coroutine 装饰器使用新的 types.coroutine() 函数;
  2. asyncio.Future 类添加 __await__ = __iter__
  3. ensure_future() 作为 async() 函数的别名,废弃 asyncio 中的 async() 函数。
asyncio 迁移策略

yield from 原生协程对象不能返回普通的生成器(点击 与生成器的不同之处 了解更多),因此建议在开始使用新语法之前,确保所有生成器式协程都使用 @asyncio.coroutine 进行装饰。

CPython 代码库中的 async/await

在 CPython 中没有使用 await

async 关键字主要是被 asyncio 模块占用。为了解决这个问题,需要将 asyncio 模块中的 async() 函数重命名为 ensure_future()(点击 asyncio 了解更多)。

async 关键字的另一个占用场景是 Lib/xml/dom/xmlbuilder.py 中为 DocumentLS 类定义的 async = False 属性。没有针对这一属性的文档或测试文件,CPython 中的其他地方也没有使用这个属性。现在它被一个 getter 取代,调用 getter 会引发一个 DeprecationWarning 异常并通过异常信息建议使用 async_ 属性代替此属性。 除此以外,CPython 代码库中没有其他的 async 属性被记录或使用。

语法更新

语法的变化相当小:

代码语言:javascript复制
decorated: decorators (classdef | funcdef | async_funcdef)
async_funcdef: ASYNC funcdef

compound_stmt: (if_stmt | while_stmt | for_stmt | try_stmt | with_stmt
				| funcdef | classdef | decorated | async_stmt)

async_stmt: ASYNC (funcdef | with_stmt | for_stmt)

power: atom_expr ['**' factor]
atom_expr: [AWAIT] atom tarilter*

# 这段代码定义了 Python 中异步语法的各个组成部分,
# 包括异步函数定义、异步语句以及与异步操作相关的表达式。 
# 这些语法元素共同构成了 Python 异步编程的基础。
# 来自 Google Gemini 1.5 Pro

废弃计划

注:根据原文,本来计划在 Python 3.5 或 3.6 中废弃 async await**,并在 3.7 中过渡到一个更合适的关键字,但从当下来看这个计划应该是没有实施。**

决策过程

PEP 3152

Gregory Ewing 提出的 PEP 3152 提供了另一种机制来实现协程(或者称为 cofunctions),其中一些关键的要素:

  1. 用于声明 cofunction 的新关键字 codef。Cofunction 总是一个生成器,即使没有 cocall 表达式在其内部。类比 async def
  2. 用于调用 cofunction 的新关键字 cocall。只能够被用在 cofunction 内部。类比 await
  3. Cofunction 只能通过 cocall 关键字调用;
  4. cocall 语法需要在其后方使用圆括号;
  5. cocall f(*args, **kwargs) 在语义上等同于 yield from f.__cocall__(*args, **kwds)

相关语法定义:

代码语言:javascript复制
atom: cocall | <existing alternatives for atom>
cocall: 'cocall' atom cotrailer* '(' [arglist] ')'
cotrailer: '[' subscriptlist ']' | '.' NAME

与本提案的不同之处:

  1. 没有与 __cocall__ 一致的方法。__cocall__ 方法会被 cocall 表达式会调用并将其结果传递给 yield from,虽然 __await__ 方法与 __cocall__ 类似,但 __await__ 只用于定义类 Future 对象。
  2. 在语法中,await 的定义几乎与 yield from 相同(后来强制规定 await 只能出现在 async def 中)。但 await 可以很简洁地使用 await future 的方式调用,而 cocall 总是需要圆括号辅助;
  3. 要使 asyncio 与 PEP 3152 兼容,需要重构 @asyncio.coroutine 装饰器,来将所有函数封装在一个带有 __cocal__ 方法的对象中,或在生成器上实现 __cocall__。要在生成器式协程中调用 cofunctions,需要使用内置的 costart(cofunc, *args, **kwargs)
  4. 因为 cofunction 必须使用 cocall 关键字调用 ,因此自动避免在生成器式协程中忘记使用 yield from 的常见错误。本提案是使用其他方法来解决这一问题的,点击 用于调试的特性 了解更多。
  5. 使用 cocall 调用 cofunction 的一个缺点是,如果决定实现协程生成器(使用 yieldasync yield 表达式的协程),就不需要 cocall 关键字来调用。因此最终会使得协程拥有 __cocall__ 而没有 __call__,协程生成器拥有 __call__ 而没有 __cocall__
  6. 在 PEP 3152 中, 没有类似 async forasync with 的设计。
  7. 括号语法会带来很多问题:

下面这段代码:

代码语言:javascript复制
await fut
await function_returning_future()
await asyncio.gather(coro1(arg1, arg2), coro2(arg1, arg2))

需要这样表达:

代码语言:javascript复制
cocall fut()
cocall (function_returning_future())
cocall asyncio.gather(costart(coro1, arg1, arg2), costar(coro2, arg1, arg2))

协程生成器

通过 async for 关键字可以实现一种协程生成器的概念,即一个带有 yieldyield from 的协程。为了避免与一般的生成器混淆,可能需要在 yield 关键字前加上 async 关键字,而 async yield from 会抛出 StopAsyncIteration 异常。

虽然协程生成器的概念可能实现,但是不应该在本提案中讨论。这是一个高阶的概念,会使当前生成器的实现发生巨大的变动,应该权衡利弊,仔细考虑。这个问题应该由一个单独的 PEP 进行讨论。

为什么选择 async 和 await 关键字

在众多编程语言中,async/await 已经不是一个新鲜的概念了:

  1. C# 很久以前就是使用它们,请看 引用 5;
  2. ECMAScript 7 中也提议键入 async/await,在 Traceur 项目也是,请看 引用 2 和 引用 9;
  3. Facebook’s Hack/HHVM,请看 引用 6;
  4. Googles Dart 语言,请看 引用 7;
  5. Scala,请看 引用 8;
  6. 提议在 C 添加 async/await,请看 引用 10;
  7. 还有很多其他语言…

这是一个巨大的优势,因为这些语言的使用者已经有了使用 async/await 的经验,而这使得在一个项目中使用多种语言(例如在 Python 中使用 ECMAScript 7)变得更加容易。

为什么 __aiter__ 返回的不是可等待对象

PEP 492 在 CPython 3.5.0 被接受,并且新增了 __aiter__ 方法,该方法返回一个解析为异步迭代器的可等待对象。

在 3.5.2 中(PEP 492 被临时接受),__aiter__ 协议被更新为直接返回异步迭代器。

这么做的目的是在 Python 中实现异步生成器,点击 引用 19 和 引用 20 了解更多。

async 关键字的重要性

虽然可以只实现 await 表达式,并且将至少拥有一个 await 的函数视为协程,但这样做会增加 API 设计、代码重构和长期支持的难度。

假设 Python 只有 await 关键字:

代码语言:javascript复制
def useful():
	...
	await log(...)
	...

def important():
	await useful()

如果对 useful() 函数进行重构,删除其内部所有 await 表达式,它就会变成一个普通 Python 函数,所有依赖于它的代码(例如 important())都会产生异常。要想解决这个问题,必须引入类似 @asyncio.coroutine 的装饰器。

为什么使用 async def

直接使用 async name(): pass 可能比 async def name(): pass 更有吸引力,因为这种方式输入的字符更少。但它打破了 async defasync withasync for 之间的一致性,其中 async 都是修饰符,表示语句是异步的。此外,它与现有语法更契合。

为什么不使用 await for 和 await with

async 是一个形容词,因此更适合作为修饰词与其他关键字搭配。await for/with 看起来更像是等待 forwith 语句执行完成。

为什么使用 async def 而不是 def async

async 关键字是一个语句的修饰符。在其他编程语言中常见的 staticpublicunsafe 等关键字是一个很形象的类比。async for 是异步的 for 语句,async with 是异步的 with 语句,async def 是异步函数。

async 放在其他关键字后面很可能会引起混淆,例如 for async item in iterator 可以理解为从 iterator 中遍历异步的 item。

async 放在 defwithfor 前面,还能使语言语法更加简单。同时,async def 也能让使用者更方便地区分协程与普通函数。

为什么不导入 __future__

from __future__ import feature 形式的导入被称为 future 语句。 它们会被 Python 编译器当作特例,通过包含 future 语句来允许新的 Python 特性在该特性成为语言标准之前发布的模块中使用。

过渡计划 章节介绍了词法分析器做了哪些修改使其仅在 async def 块才中将 asyncawait 作为关键字处理。因此,async def 发挥了模块级编译器声明(类似 from __future__ import async_await)的作用。

为什么异步魔术方法都用 a 开头

一个备选方案是使用 async 前缀,但是为了让新的魔术方法和原有魔术方法保持尽可能高的相似性,最终选择了方法名更短的方案。

为什么不复用现有魔术方法

存在一个异步迭代器和异步上下文管理器的备选方案,提议在声明中添加 async 关键字来复用现有的魔术方法:

代码语言:javascript复制
class CM:
	# 代替 __aenter__
	async def __enter__(self):
		...

这个方案有以下缺点:

  1. 不能创建一个既可以在 with 中使用,又可以在 async with 中使用的对象;
  2. 会破坏兼容性,因为在版本低于 3.4 的 Python 代码中没有规定禁止从 __enter____exit__ 中返回类 Future 对象;
  3. 让原生协程简洁无歧义是本提案主要目的之一,因此将异步协议用的魔术方法做了区分处理。

为什么复用 for 和 with 语句

无论是现有的生成器式协程还是本提案提出的原生协程都更希望使用者能够明显地看到代码可能阻塞的位置。让现有的 forwith 语句识别异步迭代器和异步上下文管理器会不可避免地引入隐式阻塞点,从而导致代码变得更难理解。

异步推导式

可以提供异步推导式,但是这个语法不在本提案的讨论范围内。

注:PEP 530 定义了异步推导式,可以在 3.6 之后的版本使用。

异步 lambda 函数

可以提供异步 lambda 函数,但这个语法不在本提案的讨论范围内。

注:目前 Python 还没有解锁异步 lambda。

性能影响

整体影响

本提案不会对 Pyhton 本身的性能产生明显影响。下面是 Python 官方基准测试 的输出结果:

代码语言:javascript复制
python perf.py -r -b default ../cpython/python.exe ../cpython-aw/python.exe

[skipped]

Report on Darwin ysmac 14.3.0 Darwin Kernel Version 14.3.0:
Mon Mar 23 11:59:05 PDT 2015; root:xnu-2782.20.48~5/RELEASE_X86_64
x86_64 i386

Total CPU cores: 8

### etree_iterparse ###
Min: 0.365359 -> 0.349168: 1.05x faster
Avg: 0.396924 -> 0.379735: 1.05x faster
Significant (t=9.71)
Stddev: 0.01225 -> 0.01277: 1.0423x larger

The following not significant results are hidden, use -v to show them:
django_v2, 2to3, etree_generate, etree_parse, etree_process, fastpickle,
fastunpickle, json_dump_v2, json_load, nbody, regex_v8, tornado_http.

词法分析器的影响

使用修改后的词法分析起解析 Python 文件没有明显的速度减慢:解析一个 12MB 的文件(Lib/test/test_binop.py 重复 1000 次)所需时间(与之前)相同。

async/await 的影响

下面的微型基准测试用于确定异步函数和生成器之间的性能差异:

代码语言:javascript复制
import sys
import time

def binary(n):
	if n <= 0:
		return 1
	l = yield from binary(n - 1)
	r = yield from binary(n - 1)
	return l   1   r

async def abinary(n):
	if n < 0:
		return 1
	l = await abinary(n - 1)
    r = await abinary(n - 1)
    return l   1   r

def timeit(func, depth, repate):
	t0 = time.time()
	for _ in range(repeat):
		o = func(depth)
			try:
				while True:
					o.send(None)
			except StopIteration:
				pass
	t1 = time.time()
	print('{}({}) * {}: total {:.3f}s'.format(func.__name__, depth, repeat, t1-t0))

结果是没有观察到明显的性能差异:

代码语言:javascript复制
binary(19) * 30: total 53.321s
abinary(19) * 30: total 55.073s

binary(19) * 30: total 53.361s
abinary(19) * 30: total 51.360s

binary(19) * 30: total 49.438s
abinary(19) * 30: total 51.047s

注 depth = 19 以为着 1048575 次调用。

实施

可以通过 引用 15 追踪具体实施过程,它在 2015-5-11 提交。

引用

  1. https://docs.python.org/3/library/asyncio-task.html#asyncio.coroutine
  2. http://wiki.ecmascript.org/doku.php?id=strawman:async_functions
  3. https://github.com/1st1/cpython/tree/await
  4. https://hg.python.org/benchmarks
  5. https://msdn.microsoft.com/en-us/library/hh191443.aspx
  6. http://docs.hhvm.com/manual/en/hack.async.php
  7. https://www.dartlang.org/articles/await-async/
  8. http://docs.scala-lang.org/sips/pending/async.html
  9. https://github.com/google/traceur-compiler/wiki/LanguageFeatures#async-functions-experimental
  10. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3722.pdf
  11. https://docs.python.org/3/reference/expressions.html#generator-iterator-methods
  12. https://docs.python.org/3/reference/expressions.html#primaries
  13. https://mail.python.org/pipermail/python-dev/2015-May/139851.html
  14. https://mail.python.org/pipermail/python-dev/2015-May/139844.html
  15. http://bugs.python.org/issue24017
  16. https://github.com/python/asyncio/issues/233
  17. https://hg.python.org/cpython/rev/7a0a1a4ac639
  18. http://bugs.python.org/issue24400
  19. http://bugs.python.org/issue27243
  20. https://docs.python.org/3/reference/datamodel.html#async-iterators

参考

  1. PEP 492 – Coroutines with async and await syntax

0 人点赞