https://www.jianshu.com/p/8cd05a23822e

关于 Python 协程的历史课

根据维基百科给出的定义,“协程 是为非抢占式多任务产生子程序的计算机程序组件,协程允许不同入口点在不同位置暂停或开始执行程序”。从技术的角度来说,“协程就是你可以暂停执行的函数”。如果你把它理解成“就像生成器一样”,那么你就想对了。

Python 2.2 引入生成器

退回到 Python 2.2,生成器第一次在PEP 255中提出(那时也把它成为迭代器,因为它实现了迭代器协议)。主要是受到Icon编程语言的启发,生成器允许创建一个在计算下一个值时不会浪费内存空间的迭代器。例如你想要自己实现一个 range() 函数,你可以用立即计算的方式创建一个整数列表:

例如:

1
2
3
4
5
6
7
8
def eager_range(up_to):
    """生成一个0到up_to大小的列表"""
    sequence = []
    index = 0
    while index < up_to:
        sequence.append(index)
        index += 1
    return sequence

然而这里存在的问题是,如果你想创建从0到1,000,000这样一个很大的序列,你不得不创建能容纳1,000,000个整数的列表。但是当加入了生成器之后,你可以不用创建完整的序列,你只需要能够每次保存一个整数的内存即可。

例如:

1
2
3
4
5
6
def lazy_range(up_to):
    """创建一个生成器,返回0到up_to的值"""
    index = 0
    while index < up_to:
        yield index
        index += 1

让函数遇到 yield 表达式时暂停执行,并且能够在后面重新执行,这对于减少内存使用、生成无限序列非常有用。

生成器在其生命周期中,会有如下四个状态
GEN_CREATED # 等待开始执行
GEN_RUNNING # 解释器正在执行(只有在多线程应用中才能看到这个状态)
GEN_SUSPENDED # 在yield表达式处暂停
GEN_CLOSED # 执行结束

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from inspect import getgeneratorstate

def func(n):
    num = 0
    while num < n:
        yield num
        num += 1

if __name__ == '__main__':
    gen = func(2)
    print(getgeneratorstate(gen))

    print(next(gen))
    print(getgeneratorstate(gen))

    print(next(gen))
    gen.close()  # 手动关闭/结束生成器
    print(getgeneratorstate(gen))

执行以上程序会输出如下结果:

1
2
3
4
5
GEN_CREATED
0
GEN_SUSPENDED
1
GEN_CLOSED

Python 2.5 引入生成器send()方法

通过上面的介绍,我们知道生成器为我们引入了暂停函数执行(yield)的功能。如果可以利用生成器“暂停”的部分,添加“将东西发送回生成器”的功能,那么 Python 突然就有了协程的概念(当然这里的协程仅限于 Python 中的概念)。将东西发送回暂停了的生成器这一特性通过 PEP 342添加到了 Python 2.5。PEP 342 为生成器引入了send()方法。这让我们不仅可以暂停生成器,而且能够传递值到生成器暂停的地方。发送的值赋值给了yield表达式,例如下面的变量jump。还是以我们的range()为例,你可以让序列向前或向后跳过几个值:

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
def jumping_range(up_to):
    """创建一个生成器,返回0到up_to的值
       send方法传递的参数,将改变生成器的所生成数值循序
    """
    index = 0
    while index < up_to:
        jump = yield index
        if jump is None:
            jump = 1
        index += jump

if __name__ == '__main__':
    iterator = jumping_range(5)
    print(next(iterator))     # 输出 0
    print(iterator.send(2))   # 通过send方法发送数字2给变量jump,输出 2
    print(next(iterator))     # 变量jump是None,设置jump等于1,输出 3
    print(iterator.send(-1))  # 通过send方法发送数字-1给变量jump,输出 2
    for x in iterator:
        print(x)              # 输出 3, 4

从语法上来看,协程和生成器类似,都是定义体中包含yield关键字的函数。
yield在协程中的用法:
.在协程中yield通常出现在表达式的右边,例如:datum = yield,可以产出值,也可以不产出–如果yield关键字后面没有表达式,那么生成器产出None.
.协程可能从调用方接受数据,调用方是通过send(datum)的方式把数据提供给协程使用,而不是next(…)函数,通常调用方会把值推送给协程。
.协程可以把控制器让给中心调度程序,从而激活其他的协程

所以总体上在协程中把yield看做是控制流程的方式。

Python 3.3 引入yield from

在Python 3.3中,通过PEP 380添加了yield from,这一特性让你能够从迭代器(生成器刚好也是迭代器)中返回任何值,从而可以以干净利索的方式重构生成器。yield from后面必须是迭代器(生成器也是迭代器)

例如:

1
2
3
4
5
6
7
8
def lazy_range(up_to):
    """创建一个生成器,返回0到up_to的值"""
    index = 0
    def gratuitous_refactor():
        while index < up_to:
            yield index
            index += 1
    yield from gratuitous_refactor()

yield from通过让重构变得简单,也让你能够将生成器串联起来,使返回值可以在调用栈中上下浮动,而不需对编码进行过多改动。

我们可以用一个使用yield和一个使用yield from的例子来对比看下。

使用yield

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 字符串
astr='ABC'
# 列表
alist=[1,2,3]
# 字典
adict={"name":"wangbm","age":18}
# 生成器
agen=(i for i in range(4,8))

def gen(*args, **kw):
    for item in args:
        for i in item:
            yield i

new_list=gen(astr, alist, adict agen)
print(list(new_list))
#输出结果 ['A', 'B', 'C', 1, 2, 3, 'name', 'age', 4, 5, 6, 7]

使用yield from

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 字符串
astr='ABC'
# 列表
alist=[1,2,3]
# 字典
adict={"name":"wangbm","age":18}
# 生成器
agen=(i for i in range(4,8))

def gen(*args, **kw):
    for item in args:
        yield from item

new_list=gen(astr, alist, adict, agen)
print(list(new_list))
#输出结果 ['A', 'B', 'C', 1, 2, 3, 'name', 'age', 4, 5, 6, 7]

从上面的例子,可见yield from item的作用相当于下面的代码块。

1
2
for i in item:
    yield i

总结
Python 2.2 中的生成器让代码执行过程可以暂停。Python 2.5中可以将值返回给暂停的生成器,这使得Python中协程的概念成为可能。加上Python 3.3 中的 yield from,可以以干净利索的方式重构生成器。

Python 3.4 引入asyncio模块

什么是事件循环?
事件循环提供一种循环机制,让你可以“在A发生时,执行B”。基本上来说事件循环就是监听当有什么发生时,同时事件循环也关心这件事并执行相应的代码。Python 3.4 以后通过标准库 asyncio获得了事件循环的特性。

asyncio 的形式出现的事件循环之间,Python 3.4 通过并发编程的形式已经对异步编程有了足够的支持。异步编程简单来说就是代码执行的顺序在程序运行前是未知的(因此才称为异步而非同步)。并发编程是代码的执行不依赖于其他部分,即便是全都在同一个线程内执行(并发不是并行)。例如,下面 Python 3.4 的代码分别以异步和并发的函数调用实现按秒倒计时。

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import asyncio

@asyncio.coroutine
def countdown(number, n):
    while n > 0:
        print('T-minus', n, '({})'.format(number))
        yield from asyncio.sleep(1)
        n -= 1

loop = asyncio.get_event_loop()
tasks = [
    asyncio.ensure_future(countdown("A", 2)),
    asyncio.ensure_future(countdown("B", 3))]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

Python 3.4 中,asyncio.coroutine 修饰器用来标记作为协程的函数,这里的协程是和asyncio及其事件循环一起使用的。这赋予了 Python 第一个对于协程的明确定义:实现了PEP 342添加到生成器中的这一方法的对象,并通过[collections.abc.Coroutine这一抽象基类]表征的对象。这意味着突然之间所有实现了协程接口的生成器,即便它们并不是要以协程方式应用,都符合这一定义。为了修正这一点,asyncio 要求所有要用作协程的生成器必须由asyncio.coroutine修饰。

有了对协程明确的定义,你可以对任何asyncio.Future对象使用yield from,从而将其传递给事件循环,暂停协程的执行来等待某些事情的发生( future 对象并不重要,只是asyncio细节的实现)。一旦 future 对象获取了事件循环,它会一直在那里监听,直到完成它需要做的一切。当 future 完成自己的任务之后,事件循环会察觉到,暂停并等待在那里的协程会通过send()方法获取future对象的返回值并开始继续执行。

以上面的代码为例。事件循环启动每一个 countdown() 协程,一直执行到遇见其中一个协程的 yield from 和 asyncio.sleep() 。这样会返回一个 asyncio.Future对象并将其传递给事件循环,同时暂停这一协程的执行。事件循环会监控这一future对象,直到倒计时1秒钟之后(同时也会检查其它正在监控的对象,比如像其它协程)。1秒钟的时间一到,事件循环会选择刚刚传递了future对象并暂停了的 countdown() 协程,将future对象的结果返回给协程,然后协程可以继续执行。这一过程会一直持续到所有的 countdown() 协程执行完毕,事件循环也被清空。稍后我会给你展示一个完整的例子,用来说明协程/事件循环之类的这些东西究竟是如何运作的,但是首先我想要解释一下async和await。

Python 3.5 引入async/await关键字,types.coroutine修饰器

在 Python 3.4 中,用于异步编程并被标记为协程的函数看起来是这样的:

1
2
3
4
# This also works in Python 3.5.
@asyncio.coroutine
def py34_coro():
    yield from stuff()

Python 3.5添加了types.coroutine修饰器(是由async def函数创建),也可以像 asyncio.coroutine一样将生成器标记为协程。Python3.5中的async相当于Python3.4的asyncio.coroutine,await相当于yield from 。上面的函数,在3.5中的写法如下。

1
2
async def py35_coro():
    await stuff()

虽然 async 和 types.coroutine 的关键作用在于巩固了协程的定义,但是它将协程从一个简单的接口变成了一个实际的类型,也使得一个普通生成器和用作协程的生成器之间的差别变得更加明确(inspect.iscoroutine() 函数 甚至明确规定必须使用 async 的方式定义才行)。

你将发现不仅仅是 async,Python 3.5 还引入 await 表达式(只能用于async def中)。虽然await的使用和yield from很像,但await可以接受的对象却是不同的。await 当然可以接受协程,因为协程的概念是所有这一切的基础。但是当你使用 await 时,其接受的对象必须是awaitable 对象:必须是定义了await()方法且这一方法必须返回一个不是协程的迭代器。协程本身也被认为是 awaitable 对象(这也是collections.abc.Coroutine 继承 collections.abc.Awaitable的原因)。这一定义遵循 Python 将大部分语法结构在底层转化成方法调用的传统,就像 a + b 实际上是a.__add__(b) 或者 b.__radd__(a)。


转载请注明本网址。