协程进化史:从yield到await

在python2.5的时候,yield关键字可以在表达式中使用,而且生成器API中增加了send()方法,也就是从这个时候开始,生成器可以当作协程使用。

在python3.3的时候,PEP380引入了yield from语句,使用它可以将复杂的生成器重构称小型嵌套生成器;而且从这个版本开始,生成器可以返回值,在此之前,在生成器中加入return语句会抛出SyntaxError。

在python3.4的时候,引入了库asyncio标准库,直接内置了对异步IO的支持。

在python3.5的时候,引入了async/await语句,让协程的实现更加的方便。

本系列文章就以上面的发展史,说说协程进化。

yield/send时代

有了前面一片文章对协程的介绍,这里我就直接贴出测试代码!

import random
import time

def fibonacci(n):
    index = 0
    a,b = 0,1
    while index < n:
        sleep_cnt = yield b
        print('let me think {} secs'.format(sleep_cnt))
        time.sleep(sleep_cnt)
        a,b = b,a+b
        index += 1

def main():
    n = 10
    sfib = fibonacci(n)
    fib_res = next(sfib)
    while True:
        print(fib_res)
        try:
            fib_res = sfib.send(random.uniform(0,0.5))
        except StopIteration:
            break

if __name__ == "__main__":
    main()

这个程序实现了斐波拉契数列的计算,其中第一次调用next(sfib)这句时,相当于slib.send(None)去预激协程。后续的sfib.send(random.uniform(0, 0.5))则将一个随机的秒数发送给sfib,作为当前中断的yield表达式的返回值。

这里主要时说明我们可以从main程序中控制协程计算斐波拉契数列的时间,而协程返回给main程序计算结果。注意,send方法发送的数据时给了sleep_cnt变量。

yield from的用处

首先,yield from可以简化yield语句!

def gen1():
    for c in "ABC":
        yield c

def gen2():
    yield from "ABC"

print(list(gen1()))
print(list(gen2()))

上面的代码输出效果是一样的,也就是说,yield只能返回一个元素;而yield from能返回一个列表,还能返回生成器、元组等。这个时候yield from表达式是对对象调用iter(),从而获取迭代器,因此,对象可以是任何可迭代的对象。

当然,yield from的功能肯定不止于此了!它的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样两者可以直接发送和产生值,还可以直接传入异常,而不用在位于中间的协程中添加大量的异常处理的样板代码。

我们现在就在yield from后面加上一个生成器,来实现生成器的嵌套。当然,实现这个生成器嵌套不一定要用yield from来实现,但是yield from可以让我们避免处理各种料想不到的异常,而让我们专注于业务代码的实现。

下面是几个概念!

1、调用方:调用委派生成器的客户端(调用方)代码
2、委托生成器:包含yield from表达式的生成器函数
3、子生成器:yield from后面加的生成器函数

下面这个示例来自《流畅的python》这本书,我觉得它比较好的解释了这三者的关系!

# 子生成器
def averager():
    total = 0
    count = 0
    average = 0
    while True:
        new_num = yield average
        count += 1
        total += new_num
        average = total/count

# 委派生成器
def grouper():
    while True:
        yield from averager()

# 调用方
def main():
    calc_average = grouper()
    next(calc_average)            # 预激下生成器
    print(calc_average.send(10))  # 打印:10.0
    print(calc_average.send(20))  # 打印:15.0
    print(calc_average.send(30))  # 打印:20.0

if __name__ == '__main__':
    main()

委托生成器的作用是:在调用方与子生成器之间建立一个双向通道。

所谓的双向通道是什么意思呢?
调用方可以通过send()直接发送消息给子生成器,而子生成器yield的值,也是直接返回给调用方。

你可能会经常看到有些代码,还可以在yield from前面看到可以赋值。这是什么用法?

你可能会以为,子生成器yield回来的值,被委托生成器给拦截了。你可以亲自写个demo运行试验一下,并不是你想的那样。
因为我们之前说了,委托生成器,只起一个桥梁作用,它建立的是一个双向通道,它并没有权利也没有办法,对子生成器yield回来的内容做拦截。

为了解释这个用法,我还是用上述的例子,并对其进行了一些改造。添加了一些注释,希望你能看得明白。

# 子生成器
def averager():
    total = 0
    count = 0
    average = 0
    while True:
        new_num = yield average
        if new_num is None:
            break
        count += 1
        total += new_num
        average = total/count

    # 每一次return,都意味着当前协程结束。
    return total,count,average

# 委派生成器
def grouper():
    while True:
        # 只有子生成器要结束(return)了,yield from左边的变量才会被赋值,后面的代码才会执行。
        total, count, average = yield from averager()
        print("计算完毕!!\n总共传入 {} 个数值, 总和:{},平均数:{}".format(count, total, average))

# 调用方
def main():
    calc_average = grouper()
    next(calc_average)            # 预激协程
    print(calc_average.send(10))  # 打印:10.0
    print(calc_average.send(20))  # 打印:15.0
    print(calc_average.send(30))  # 打印:20.0
    calc_average.send(None)      # 结束协程
    # 如果此处再调用calc_average.send(10),由于上一协程已经结束,将重开一协程

if __name__ == '__main__':
    main()

输出如下:

10.0
15.0
20.0
计算完毕!!
总共传入 3 个数值, 总和:60,平均数:20.0

那么这个时候问题就来了,为什么我们不直接调用子生成器不就得了,要这个委派生成器干啥啊?不是多此一举吗?

如果我们直接调用子生成器,就需要自己实现异常的捕捉了。具体代码实现如下。

# 子生成器
def averager():
    total = 0
    count = 0
    average = 0
    while True:
        new_num = yield average
        if new_num is None:
            break
        count += 1
        total += new_num
        average = total/count
    return total,count,average

# 调用方
def main():
    calc_average = averager()
    next(calc_average)            # 预激协程
    print(calc_average.send(10))  # 打印:10.0
    print(calc_average.send(20))  # 打印:15.0
    print(calc_average.send(30))  # 打印:20.0

    # ----------------注意-----------------
    try:
        calc_average.send(None)
    except StopIteration as e:
        total, count, average = e.value
        print("计算完毕!!\n总共传入 {} 个数值, 总和:{},平均数:{}".format(count, total, average))
    # ----------------注意-----------------

if __name__ == '__main__':
    main()

这段代码和上面实现了委派生成器的代码看起来差别也不大啊,自己实现也没有什么大不了的啊,那你看看在内部yield from为我们实现了多少东西!

实现yield from语法的为代码如下:

"""
_i:子生成器,同时也是一个迭代器
_y:子生成器生产的值
_r:yield from 表达式最终的值
_s:调用方通过send()发送的值
_e:异常对象
"""

_i = iter(EXPR)

try:
    _y = next(_i)
except StopIteration as _e:
    _r = _e.value

else:
    while 1:
        try:
            _s = yield _y
        except GeneratorExit as _e:
            try:
                _m = _i.close
            except AttributeError:
                pass
            else:
                _m()
            raise _e
        except BaseException as _e:
            _x = sys.exc_info()
            try:
                _m = _i.throw
            except AttributeError:
                raise _e
            else:
                try:
                    _y = _m(*_x)
                except StopIteration as _e:
                    _r = _e.value
                    break
        else:
            try:
                if _s is None:
                    _y = next(_i)
                else:
                    _y = _i.send(_s)
            except StopIteration as _e:
                _r = _e.value
                break
RESULT = _r

以上的代码,稍微有点复杂,有兴趣的同学可以结合以下说明去研究看看。

  1. 迭代器(即可指子生成器)产生的值直接返还给调用者
  2. 任何使用send()方法发给委派生产器(即外部生产器)的值被直接传递给迭代器。如果send值是None,则调用迭代器next()方法;如果不为None,则调用迭代器的send()方法。如果对迭代器的调用产生StopIteration异常,委派生产器恢复继续执行yield from后面的语句;若迭代器产生其他任何异常,则都传递给委派生产器。
  3. 子生成器可能只是一个迭代器,并不是一个作为协程的生成器,所以它不支持.throw()和.close()方法,即可能会产生AttributeError 异常。
  4. 除了GeneratorExit 异常外的其他抛给委派生产器的异常,将会被传递到迭代器的throw()方法。如果迭代器throw()调用产生了StopIteration异常,委派生产器恢复并继续执行,其他异常则传递给委派生产器。
  5. 如果GeneratorExit异常被抛给委派生产器,或者委派生产器的close()方法被调用,如果迭代器有close()的话也将被调用。如果close()调用产生异常,异常将传递给委派生产器。否则,委派生产器将抛出GeneratorExit 异常。
  6. 当迭代器结束并抛出异常时,yield from表达式的值是其StopIteration 异常中的第一个参数。
  7. 一个生成器中的return expr语句将会从生成器退出并抛出 StopIteration(expr)异常。

asyncio来了

在学习asyncio库之前,我们需要熟悉几个概念。

·  event_loop 事件循环:程序开启一个无限的循环,程序员会把一些函数(协程)注册到事件循环上。当满足事件发生的时候,调用相应的协程函数。

·   coroutine 协程:协程对象,指一个使用async关键字定义的函数,它的调用不会立即执行函数,而是会返回一个协程对象。协程对象需要注册到事件循环,由事件循环调用。

·  future 对象: 代表将来执行或没有执行的任务的结果。它和task上没有本质的区别。

·  task 任务:一个协程对象就是一个原生可以挂起的函数,任务则是对协程进一步封装,其中包含任务的各种状态。Task 对象是 Future 的子类,它将 coroutine 和 Future 联系在一起,将 coroutine 封装成一个 Future 对象。

下面我们利用实例说明asyncio与yield from结合的用法!

import asyncio
from collections.abc import Generator,Coroutine

@asyncio.coroutine
def fib(n):
    index = 0
    a,b = 0,1
    while index < n:
        yield from asyncio.sleep(1)
        print(b)
        a,b = b,a+b
        index += 1

if __name__ == "__main__":
    coro = fib(6)
    print(isinstance(coro,Generator))
    print(isinstance(coro,Coroutine))
    #定义事件循环对象容器
    loop = asyncio.get_event_loop()
    #将协程转化为task任务
    task = loop.create_task(coro)
    #将task任务扔进事件循环对象中并触发
    loop.run_until_complete(task)

 

我们利用isinstance函数来判断是否创建的是生成器对象,其中利用@asyncio.coroutine装饰器,将函数标记为一个协程对象,至于为什么只是标记?在后面的输出中,可以看到标记后的对象其实是一个生成器,但是实际上,可以当作协程使用。

例中yield from后面接的asyncio.sleep()是一个coroutine(里面也用了yield from),所以线程不会等待asyncio.sleep(),而是直接中断并执行下一个消息循环。当asyncio.sleep()返回时,线程就可以从yield from拿到返回值(此处是None),然后接着执行下一行语句。 asyncio是一个基于事件循环的实现异步I/O的模块。通过yield from,我们可以将协程asyncio.sleep的控制权交给事件循环,然后挂起当前协程;之后,由事件循环决定何时唤醒asyncio.sleep,接着向后执行代码。

协程之间的调度都是由事件循环决定。

yield from asyncio.sleep(sleep_secs) 这里不能用time.sleep(1)因为time.sleep()返回的是None,它不是iterable,还记得前面说的yield from后面必须跟iterable对象(可以是生成器,迭代器)。

async/await的到来

前面的yield from是比较容易不让人理解的,很早的时候就有人提出引入新的关键字了,于是在3.5的版本中,正式引入了async/await关键字。我们可以把它简单的理解为@asyncio.coroutine与yield from的替代品,提取也是如此。

我们将上面的代码用async/await改写,如下!

import asyncio
from collections.abc import Generator,Coroutine

async def fib(n):
    index = 0
    a,b = 0,1
    while index < n:
        await asyncio.sleep(1)
        print(b)
        a,b = b,a+b
        index += 1

if __name__ == "__main__":
    coro = fib(6)
    print(isinstance(coro,Generator))
    print(isinstance(coro,Coroutine))
    #定义事件循环对象容器
    loop = asyncio.get_event_loop()
    #将协程转化为task任务
    task = loop.create_task(coro)
    #将task任务扔进事件循环对象中并触发
    loop.run_until_complete(task)

基本是一样的,但是输出的时候,用async修饰的对象就是一个协程了,isinstance(coro,Coroutine)这句显示为True。

这个流程是这样的,1、定义/创建协程对象;2、定义事件循环对象容器;3、将协程转化为task对象;4、将task任务扔进事件循环对象中触发。

对于上面的步骤,我们可以将第3步改写为,利用task=asyncio.ensure_future(coro)来实现将协程转化为task对象,此时的对象准确说是Future对象。前面我们说过,Task是Future的子类。

我们的异步IO的实现就是会挂起耗时的IO,等IO结束时,继续执行IO后面的代码,这个时候我们就需要依赖IO的返回值了,这个时候就需要用到回调了。

回调的实现有以下两种,1、利用同步编程实现回调;2、asyncio自带的添加回调函数功能实现。

#利用同步编程实现回调
import  asyncio
import time

async def _sleep(x):
    time.sleep(2)
    return '暂停了{}秒!'.format(x)

coro = _sleep(2)

loop = asyncio.get_event_loop()

task = asyncio.ensure_future(coro)
loop.run_until_complete(task)
# task.result() 可以取得返回结果
print('返回结果:{}'.format(task.result()))
#asyncio自带的添加回调函数功能实现
import  asyncio
import time

async def _sleep(x):
    time.sleep(2)
    return '暂停了{}秒!'.format(x)

def callback(future):
    print('这里是回调函数,获取返回结果是:',future.result())

coro = _sleep(2)
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(coro)
task.add_done_callback(callback)
loop.run_until_complete(task)

协程的目的就是为了解决并发的问题,貌似现在并发也没怎么看到啊,下面我们利用代码实现事发实例。

import  asyncio

async def do_some_work(x):
    print('Waiting:',x)
    await asyncio.sleep(x)
    return 'Done after {}s!'.format(x)

coro1 = do_some_work(1)
coro2 = do_some_work(2)
coro3 = do_some_work(3)

tasks = [
    asyncio.ensure_future(coro1),
    asyncio.ensure_future(coro2),
    asyncio.ensure_future(coro3),
]

loop = asyncio.get_event_loop()
#将协程注册到事件循环中
loop.run_until_complete(asyncio.wait(tasks))
#loop.run_until_complete(asyncio.gather(*tasks))
for task in tasks:
    print("Task ret:",task.result())

注意我们有两种方式将协程注册到事件循环中。

使用async可以定义协程,协程用于耗时的io操作,我们也可以封装更多的io操作过程,这样就实现了嵌套的协程,即一个协程中await了另外一个协程,如此连接起来。注意在await时也是两种写法!

import  asyncio

async def do_some_work(x):
    print('Waiting:',x)
    await asyncio.sleep(x)
    return 'Done after {}s!'.format(x)

async def main():
    coro1 = do_some_work(1)
    coro2 = do_some_work(2)
    coro3 = do_some_work(3)

    tasks = [
    asyncio.ensure_future(coro1),
    asyncio.ensure_future(coro2),
    asyncio.ensure_future(coro3),
    ]

    #await一个task列表
    dones,pending = await asyncio.wait(tasks)
    #results = await asyncio.gather(*tasks)
    #for result in results:
    for task in dones:
        print("Task ret:",task.result())

loop = asyncio.get_event_loop()
#将协程注册到事件循环中
loop.run_until_complete(main())

我们的协程也是有如下这些状态的!

Pending:创建future,还未执行
Running:事件循环正在调用执行任务
Done:任务执行完毕
Cancelled:Task被取消后的状态

import  asyncio
import threading
import time

async def hello():
    print("Running in the loop...")
    flag = 0
    while flag < 1000:
        with open("./test.txt","a") as f:
            f.write("******")
        flag += 1
    print("stop the loop")

if __name__ == "__main__":
    coro = hello()
    loop = asyncio.get_event_loop()
    task = loop.create_task(coro)

    print(task)
    try:
        t = threading.Thread(target=loop.run_until_complete,args=(task,))
        t.start()

        time.sleep(1)
        print(task)
        t.join()
    except KeyboardInterrupt as e:
        task.cancel()
        print(task)
    finally:
        print(task)

我们可以看到顺序执行,打印pending->Running->finished。如果执行的时候按下键,会打印出Cancelling状态。

下面,我们介绍如何将协程添加到事件循环中!前者主线程同步,后者主线程异步!

import asyncio
import time
from queue import Queue
from threading import Thread

def start_loop(loop):
    asyncio.set_event_loop(loop)
    loop.run_forever()

def sleep(x,queue,msg=""):
    time.sleep(x)
    queue.put(msg)

queue = Queue()
new_loop = asyncio.new_event_loop()

#定义一个线程,并传入一个事件循环对象
t = Thread(target=start_loop,args=(new_loop,))
t.start()

print(time.ctime())

#动态添加两个协程,这种方式添加,在主线程中是同步的
new_loop.call_soon_threadsafe(sleep,6,queue,"first")
new_loop.call_soon_threadsafe(sleep,3,queue,"second")

while True:
    msg = queue.get()
    print("{} done!".format(msg))
    print(time.ctime())

#Sat Jun 29 00:40:38 2019
#first done!
#Sat Jun 29 00:40:44 2019
#second done!
#Sat Jun 29 00:40:47 2019
import asyncio
import time
from queue import Queue
from threading import Thread

def start_loop(loop):
    asyncio.set_event_loop(loop)
    loop.run_forever()

async def sleep(x,queue,msg=""):
    await asyncio.sleep(x)
    queue.put(msg)

queue = Queue()
new_loop = asyncio.new_event_loop()

#定义一个线程,并传入一个事件循环对象
t = Thread(target=start_loop,args=(new_loop,))
t.start()

print(time.ctime())

#动态添加两个协程,这种方式添加,在主线程中是异步的
asyncio.run_coroutine_threadsafe(sleep(6,queue,"first"),new_loop)
asyncio.run_coroutine_threadsafe(sleep(3,queue,"second"),new_loop)

while True:
    msg = queue.get()
    print("{} done!".format(msg))
    print(time.ctime())

#Sat Jun 29 00:45:25 2019
#second done!
#Sat Jun 29 00:45:28 2019
#first done!
#Sat Jun 29 00:45:31 2019

参考文章

《流畅的python》

https://cuiqingcai.com/6160.html

https://www.jianshu.com/p/0f86708353b3

https://blog.csdn.net/mieleizhi0522/article/details/82142856

https://juejin.im/post/5b3af9fb51882507d4487144

http://www.woola.net/detail/2016-10-18-python-coprocessor.html

https://stackoverflow.com/questions/231767/what-does-the-yield-keyword-do

你可能感兴趣的:(Python)