在python2.5的时候,yield关键字可以在表达式中使用,而且生成器API中增加了send()方法,也就是从这个时候开始,生成器可以当作协程使用。
在python3.3的时候,PEP380引入了yield from语句,使用它可以将复杂的生成器重构称小型嵌套生成器;而且从这个版本开始,生成器可以返回值,在此之前,在生成器中加入return语句会抛出SyntaxError。
在python3.4的时候,引入了库asyncio标准库,直接内置了对异步IO的支持。
在python3.5的时候,引入了async/await语句,让协程的实现更加的方便。
本系列文章就以上面的发展史,说说协程进化。
有了前面一片文章对协程的介绍,这里我就直接贴出测试代码!
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语句!
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
以上的代码,稍微有点复杂,有兴趣的同学可以结合以下说明去研究看看。
在学习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对象(可以是生成器,迭代器)。
前面的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