python generators, coroutines, native coroutines and async/await

Abstraction is not about vagueness, it is about being precise at a new semantic level. - Dijkstra

笔者之前学习python的时候就对这几个概念有些困惑,尤其是python3之后又不断添加了yield from, async, await等关键字用来支持异步编程。最近看到一篇比较好的博客就结合自己的理解翻译并解释一下这些概念,包括生成器,协程,原生协程和python3.5引入的async/await。 请使用python3.5运行代码示例。


Generators(生成器)

python中生成器是用来生成值的函数。通常函数使用return返回值然后作用域被销毁,再次调用函数会重新执行。但是生成器可以yield一个值之后暂停函数执行,然后控制权交给调用者,之后我们可以恢复其执行并且获取下一个值,我们看一个例子:

def simple_gen():
    yield 'hello'
    yield 'world'

gen = simple_gen()
print(type(gen))    # <class 'generator'>
print(next(gen))    # 'hello'
print(next(gen))    # 'world'

注意生成器函数调用的时候不会直接返回值,而是返回一个类似于可迭代对象(iterable)的生成器对象(generator object),我们可以对生成器对象调用next()函数来迭代值,或者使用for循环。
生成器常用来节省内存,比如我们可以使用生成器函数yield值来替代返回一个耗费内存的大序列:

def f(n):
    res = []
    for i in range(n):
        res.append(i)
    return res

def yield_n(n):
    for i in range(n):
        yield i

Coroutines(协程)

上一节讲到了使用使用生成器来从函数中获取数据(pull data),但是如果我们想发送一些数据呢(push data)?这时候协程就发挥作用了。yield关键字既可以用来获取数据也可以在函数中作为表达式(在=右边的时候)。我们可以对生成器对象使用send()方法来给函数发送值。这叫做『基于生成器的协程』(generator based coroutines),下边是一个示例:

def coro():
    hello = yield 'hello'    # yield关键字在=右边作为表达式,可以被send值
    yield hello


c = coro()
print(next(c))    # 输出 'hello'
print(c.send('world'))    # 输出 'world'

这里发生了什么?和之前一样我们先调用了next()函数,代码执行到yield 'hello'然后我们得到了’hello’之后我们使用了send函数发送了一个值’world’, 它使coro恢复执行并且赋了参数’world’给hello这个变量,接着执行到下一行的yield语句并将hello变量的值’world’返回。所以我们得到了send()方法的返回值’world’。

当我们使用基于生成器的协程(generator based coroutines)时候,术语”generator”和”coroutine”通常表示一个东西,尽管实际上不是。而python3.5以后增加了async/await关键字用来支持原生协程(native coroutines),我们在后边讨论。


Async I/O and the asyncio module (异步IO和asyncio模块)

python3.4以后标准库增加了新的asyncio模块来支持更加简洁的异步编程。我们可以在asyncio模块使用协程轻松实现异步IO,下边是一个来自官方文档的示例:

import asyncio
import datetime
import random


@asyncio.coroutine
def display_date(num, loop):
    end_time = loop.time() + 50.0
    while True:
        print('Loop: {} Time: {}'.format(num, datetime.datetime.now()))
        if (loop.time() + 1.0) >= end_time:
            break
        yield from asyncio.sleep(random.randint(0, 5))


loop = asyncio.get_event_loop()
asyncio.ensure_future(display_date(1, loop))
asyncio.ensure_future(display_date(2, loop))
loop.run_forever()

我们创建了一个协程display_date(num, loop),它接收一个数字和event loop作为参数,然后持续输出当前时间。然后使用yield from关键字来await从asyncio.sleep()执行的结果。asyncio.sleep()是一个协程,在指定时间以后完成。之后我们在默认的事件循环(event loop)中使用asyncio.ensure_future()来调度协程的执行,最后通知事件循环一直执行下去。

如果我们执行这段代码,可以看到两个协程是并发执行的。当我们用yield from的时候,事件循环知道它将(这里指sleep函数)将会忙碌一段时间然后暂停这个协程的执行转而执行另一个协程。所以这两个协程能够并发执行(注意并发不是并行,因为event loop是单线程的,所以不是真正意义上的『同时执行』)。

这里只需要知道yield from是一个语法糖用来替代下边这种形式的写法,这种形式使代码更加简洁。

# yield from 等价方式 yield from asyncio.sleep(random.randint(0, 5))
for x in asycio.sleep(random.randint(0, 5)):
    yield x

Native Coroutines and async/await (原生线程与async/await)

记住到目前为止,我们仍然使用的是 基于生成器的协程(generators based coroutines),在python3.5中,python增加了使用async/await语法的原生协程(native coroutines)。之前的函数用async/await语法可以这么写:

import asyncio
import datetime
import random

async def display_date(num, loop):
    end_time = loop.time() + 50.0
    while True:
        print('Loop: {} Time: {}'.format(num, datetime.datetime.now()))
        if (loop.time() + 1.0) >= end_time:
            break
        await asyncio.sleep(random.randint(0, 5))


loop = asyncio.get_event_loop()
asyncio.ensure_future(display_date(1, loop))
asyncio.ensure_future(display_date(2, loop))
loop.run_forever()

你能看出变化吗?实际上就是去掉了装饰器`@asyncio.coroutine,然后在定义前加上async关键字,之后把yield from替换成await`。写法是不是更加简洁了?


Native vs Generator Based Coroutines: Interoperability (原生协程 vs 基于生成器的协程)

实际上除了语法之外原生协程(async/await)和基于生成器的协程(@asyncio.coroutine/yield from)并没有功能上的区别。但是注意,这两种写法不能混用,就是说你不能在generator based coroutines里使用await,或者在naive coroutines里头使用yield或者yield from

除此之外,两种写法是互通的,我们可以同时使用,比如我们可以在原生协程里await一个基于生成器的协程,也可以在基于生成器的协程里yield from一个使用async定义的原生协程。
比如我们同时在一个时间循环里使用两种协程:

import asyncio
import datetime
import random
import types


@types.coroutine
def my_sleep_func():
    yield from asyncio.sleep(random.randint(0, 5))    # 注意这里就不能用 await


async def display_date(num, loop):
    end_time = loop.time() + 50.0
    while True:
        print('Loop: {} Time: {}'.format(num, datetime.datetime.now()))
        if (loop.time() + 1.0) >= end_time:
            break
        await my_sleep_func()    # 注意这里就不能用 async


loop = asyncio.get_event_loop()
asyncio.ensure_future(display_date(1, loop))
asyncio.ensure_future(display_date(2, loop))
loop.run_forever()

Ref

python: generators, coroutines, native coroutines and async/await

How the heck does async/await work in Python 3.5?