协程
to yield 含义:产出和让步。
yield item这行代码会产出一个值,提供给next(...)的调用方;此外,还会作出让步,暂停执行生成器,让调用方继续工作,直到需要使用另一个值时再调用next()。调用方会从生成器中拉取值。
从根本上把yield视作控制流程的方式,这样就好理解协程了。
将生成器当作一个协程
生成器的调用方可以使用.send(...)方法发送数据,发送的数据会成为生成器函数中yield表达式的值。因此,生成器可以作为协程使用。协程是指一个过程,这个过程与调用方协作,产出由调用方提供的值。
def simple_coroutine():
print("->coroutine started")
x = yield # ①
print("-> coroutine received:", x)
my_coro = simple_coroutine()
next(my_coro) # ②
my_coro.send(42) # ③
# 输出:
->coroutine started
Traceback (most recent call last):
-> coroutine received: 42
File "C:/Users/42072/PycharmProjects/day01/ready04/yield_reday.py", line 62, in <module>
my_coro.send(42)
StopIteration
- ① yield在表达式中使用;如果协程只需从客户那里接收数据,那么产出的值是None——这个值是隐式指定的,因为yield关键字右边没有表达式。
- ② 首先要调用next(...)函数,因为生成器还没启动,没在yield语句处暂停,所以一开始无法发送数据。
- ③ 调用这个方法后,协程定义体中的yield表达式会计算出42;现在,协程会恢复,一直运行到下一个yield表达式,或者终止。
协程的状态
- GEN_CREATED:等待开始执行
- GEN_RUNNING:解释器正在执行
- GEN_SUSPENDED:在yield表达式处暂停
- GEN_CLOSED:执行结束
from inspect import getgeneratorstate
def simple_coroutine():
print("->coroutine started")
x = yield
print(getgeneratorstate(my_coro))
print("-> coroutine received:", x)
my_coro = simple_coroutine()
print(getgeneratorstate(my_coro))
next(my_coro)
print(getgeneratorstate(my_coro))
my_coro.send(42)
print(getgeneratorstate(my_coro))
注意:
- 因为send方法的参数会成为暂停的yield表达式的值,所以,仅当协程处于暂停状态时才能调用send方法。
- 如果给未激活的协程对象发送None以外的值,会引发错误。
激活协程的方式有两种:
- next(my_coro)方法
- my_coro.send(None)
示例:协程产出两个值
def simple_coro2(a):
print('->Started :a = ' ,a)
b = yield a
print('->Started :b = ' ,b)
c = yield a + b
print('->Received: c=',c)
my_coro2 = simple_coro2(14)
next(my_coro2)
rs1 = my_coro2.send(28)
print(rs1)
rs2 = my_coro2.send(99)
print(rs2)
案例:计算移动平均值
def averager():
total = 0.0
count = 0
average = None
while True:
term = yield average
total += term
count += 1
average = total / count
coro_avg = averager()
next(coro_avg)
print(coro_avg.send(10))
print(coro_avg.send(20))
print(coro_avg.send(30))
#输出:
10.0
15.0
20.0
使用协程之前必须预激,可是这一步容易忘记。为了避免忘记,可以在协程上使用一个特殊的装饰器。
- 额外知识:wraps
作者:hqzxsc2006
来源:CSDN
原文:https://blog.csdn.net/hqzxsc2006/article/details/50337865
Python装饰器(decorator)在实现的时候,被装饰后的函数其实已经是另外一个函数了(函数名等函数属性会发生改变),为了不影响,Python的functools包中提供了一个叫wraps的decorator来消除这样的副作用。写一个decorator的时候,最好在实现之前加上functools的wrap,它能保留原有函数的名称和docstring。
#不加wraps
def my_decorator(func):
def wrapper(*args, **kwargs):
"""decorator"""
print("Calling decorated function...")
return func(*args,**kwargs)
return wrapper
@my_decorator
def example():
"""Docstring"""
print('Called example function')
print(example.__name__,example.__doc__)
#输出:
wrapper decorator
#加wraps
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
"""decorator"""
print("Calling decorated function...")
return func(*args,**kwargs)
return wrapper
@my_decorator
def example():
"""Docstring"""
print('Called example function')
print(example.__name__,example.__doc__)
#输出:
example Docstring
预激协程
协程如果不激活,那么则没什么用。调用send()方法之前,需要先调用next(my_coro)方法进行激活。为了简化协程的用法,有时会使用一个预激装饰器。
from functools import wraps
def coroutine(func):
@wraps(func)
def primer(*args,**kwargs):
gen = func(*args,**kwargs)
next(gen)
return gen
return primer
@coroutine
def averager():
total = 0.0
count = 0
average = None
while True:
term = yield average
total += term
count += 1
average = total / count
from inspect import getgeneratorstate
coro_avg = averager()
print(getgeneratorstate(coro_avg))
print(coro_avg.send(10))
print(coro_avg.send(30))
输出:
GEN_SUSPENDED
10.0
20.0
如果使用yield from 语法,会自动预激。asyncio.coroutine装饰器不会预激活协程,因此能兼容yield from 句法。
终止协程和异常处理
协程中未处理的异常会向上冒泡,传给next函数或send方法的调用方。
例如:
coro_avg = averager()
print(coro_avg.send(20))
print(coro_avg.send('aa'))
- 由于第三行发送的不是数据,导致协程内部抛出异常。
- 由于协程内部没有处理异常,协程会终止。如果试图重新激活协程,会抛出StopIteration异常。
python 2.5以后,客户代码可以调用以下两个方法,显示地把异常发给协程:
- generator.throw(exec_type[,exc_value[,traceback]])
致使生成器在暂停的yield表达式处抛出指定的异常。如果生成器处理了抛出的异常,代码会向前执行到下一个yield表达式,而产出的值会成为调用generator.throw方法得到的返回值。如果生成器没有处理抛出的异常,异常会向上冒泡,传到调用方的上下文中。
- generator.close()
致使生成器在暂停的yield表达式处抛出GeneratorExit异常。如果生成器没有处理这个异常,或者抛出了StopIteration异常(通常是指运行到结尾),调用方不会报错。如果收到GeneratorExit异常,生成器一定不能产出值,否则解释器会抛出RuntimeError异常。生成器抛出的其他异常会向上冒泡,传给调用方。
示例:调用
class DemoException(Exception):
"""自定义异常"""
def demo_exc_handling():
print("-> oroutine started")
while True:
try :
x = yield
except DemoException:
print('*** DemoException handled. Continuing...')
else :
print('-> coroutine received: {!r}'.format(x))
raise RuntimeError('永远不执行')
exc_coro = demo_exc_handling()
next(exc_coro)
exc_coro.send(11)
exc_coro.send(22)
exc_coro.close()
from inspect import getgeneratorstate
print(getgeneratorstate(exc_coro))
如果传入 DemoException,则协程会正常处理,并继续运行。
exc_coro = demo_exc_handling()
next(exc_coro)
exc_coro.send(11)
exc_coro.send(22)
# 将 DemoException 传入
exc_coro.throw(DemoException)
from inspect import getgeneratorstate
print(getgeneratorstate(exc_coro))
如果传入的异常没有处理,协程会立即停止,变成'GEN_CLOSED'状态。
exc_coro = demo_exc_handling()
next(exc_coro)
exc_coro.send(11)
exc_coro.send(22)
# 将 DemoException 传入
exc_coro.throw(ZeroDivisionError)
from inspect import getgeneratorstate
print(getgeneratorstate(exc_coro))
如果协程必须做一些清理工作,则可以在协程体中放入 try/finally 代码块。
def demo_finally():
print('-> coroutine started')
try:
while True:
try:
x = yield
except DemoException:
print('*** DemoException handled. Continuing...')
else:
print('-> coroutine received: {!r}'.format(x))
finally:
print("->coroutine ending...")
demo_coro = demo_finally();
next(demo_coro)
demo_coro.send(11)
demo_coro.send(ZeroDivisionError)
让协程返回值
协程可以在执行时不产出值,而是在最后返回一个值(通常是累计值)。
例子:一次返回平均值
from collections import namedtuple
Result = namedtuple('Result','count average')
def averager():
total = 0.0
count = 0
average = None
while True:
term = yield
if term is None:
break
total += term
count += 1
average = total / count
return Result(count,average)
coro_avg = averager();
next(coro_avg)
coro_avg.send(10)
coro_avg.send(30)
coro_avg.send(20)
coro_avg.send(None)
#输出:
Traceback (most recent call last):
File "C:/Users/42072/PycharmProjects/day01/ready04/cor.py", line 109, in <module>
coro_avg.send(None)
StopIteration: Result(count=3, average=20.0)
发送None会终止循环,导致协程结束,返回结果。一如既往,生成器对象会抛出StopIteration异常。异常对象的value属性保存着返回的值。
coro_avg = averager();
next(coro_avg)
coro_avg.send(10)
coro_avg.send(30)
coro_avg.send(20)
try:
coro_avg.send(None)
except StopIteration as exc:
result = exc.value
print(result)
yield from结构会在内部自动捕获StopIteration异常。对yield from结构来说,解释器不仅会捕获StopIteration异常,还会把value属性的值变成yield from表达式的值。
yield from 句法
在生成器gen中使用yield from subgen()时,subgen会获得控制权,把产出的值传给gen的调用方,即调用方可以直接控制subgen。与此同时,gen会阻塞,等待subgen终止。
用来简化for循环中的yield表达式
def gen():
for c in 'AB':
yield c
for i in range(1,3):
yield i
print(list(gen()))
# 可以改写为:
def gen():
yield from 'AB'
yield from range(1,3)
print(list(gen()))
yield from x表达式对x对象所做的第一件事是,调用iter(x),从中获取迭代器。因此,x可以是任何可迭代的对象。
把职责委托给子生成器
yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样二者可以直接发送和产出值,还可以直接传入异常,而不用在位于中间的协程中添加大量处理异常的样板代码。通过这个结构,协程可以把功能委托给子生成器。
主要术语:
委派生成器:
包含 yield from <iterable> 表达式的生成器函数。子生成器:
从 yield from 表达式中 <iterable>部分获取的生成器。调用方
调用委派生成器的客户端代码。
graph LR
调用方-->委派生成器
委派生成器-->子生成器
示例:
计算7年级学生的体重和身高的平均值:
from collections import namedtuple
Result = namedtuple('Result','count average')
# 子生成器
def averager():
total = 0.0
count = 0
average = None
while True:
# main 方法中发送的各种值,会绑定到term变量上
term = yield
# 子生成器终止的条件
if term is None:
break
total += term
count += 1
average = total / count
# 返回值会成为grouper中 yield from表达式的值
return Result(count,average)
# 委派生成器
def grouper(results,key):
while True:
# 每次迭代都会生成一个averager实例。每个生成器都是本协程(grouper)使用的生成器对象。
results[key] = yield from averager()
# 客户端代码
def main(data):
results = {}
for key,values in data.items():
# results 用来存储结果
group = grouper(results, key)
# 预激活协程
next(group)
for value in values:
# 发送的每个值都会经由grouper的yield from处理,通过管道传给averager实例。同时,当前的grouper实例,会在yield from 处暂停。
group.send(value)
# 把None值传入grouper,导致当前的averager实例终止,并让grouper继续运行,再创建一个aveager实例,处理下一组值。
group.send(None)
print(results)
data = {
'girls;kg':[40.9,38.5,44.3,42.2,45.2,41.7,44.5,38.0,40.6,44.5],
'girls;m':[1.6,1.51,1.4,1.3,1.41,1.39,1.33,1.46,1.45,1.43],
'boys;kg':[39.0,40.8,43.2,40.8,43.1,38.6,41.4,40.6,36.3],
'boys;m':[1.38,1.5,1.32,1.25,1.37,1.48,1.25,1.49,1.46]
}
main(data)
# 输出:
{'girls;kg': Result(count=10, average=42.040000000000006), 'girls;m': Result(count=10, average=1.4279999999999997), 'boys;kg': Result(count=9, average=40.422222222222224), 'boys;m': Result(count=9, average=1.3888888888888888)}
委派生成器在yield from 表达式处暂停时,调用方可以直接把数据发给子生成器,子生成器再把产出的值发给调用方。子生成器返回之后,解释器会抛出StopIteration异常,并把返回值附加到异常对象上,此时委派生成器会恢复运行。
注意:
- 如果子生成器不终止,委派生成器会在yield from处永远暂停。
- 因为委派生成器相当于管道,所以可以把任意数量个委派生成器连接在一起:一个委派生成器使用yield from调用一个子生成器,而那个子生成器本身也是委派生成器,使用yield from调用另一个子生成器,以此类推。最终,这个链条要以一个只使用yield表达式的简单生成器结束;不过,也能以任何可迭代的对象结束。
- 任何yield from链条都必须由客户驱动,在最外层委派生成器上调用next(...)函数或.send(...)方法。
yield from的意义
- 子生成器产出的值都直接传给委派生成器的调用方(即客户端代码)。
- 使用send()方法发给委派生成器的值都直接传给子生成器。如果发送的值是None,那么会调用子生成器的next()方法。如果发送的值不是None,那么会调用子生成器的send()方法。如果调用的方法抛出StopIteration异常,那么委派生成器恢复运行。任何其他异常都会向上冒泡,传给委派生成器。
- 生成器退出时,生成器(或子生成器)中的return expr表达式会触发StopIteration(expr)异常抛出。
- yield from表达式的值是子生成器终止时传给StopIteration异常的第一个参数。
- 传入委派生成器的异常,除了GeneratorExit之外都传给子生成器的throw()方法。如果调用throw()方法时抛出StopIteration异常,委派生成器恢复运行。StopIteration之外的异常会向上冒泡,传给委派生成器。
- 如果把GeneratorExit异常传入委派生成器,或者在委派生成器上调用close()方法,那么在子生成器上调用close()方法,如果它有的话。如果调用close()方法导致异常抛出,那么异常会向上冒泡,传给委派生成器;否则,委派生成器抛出GeneratorExit异常。
协程能自然地表述很多算法,例如仿真、游戏、异步I/O,以及其他事件驱动型编程形式或协作式多任务。
案例:出租车运营仿真
import collections
# time 事件发生时间 proc:出租车编号 action:活动描述
Event = collections.namedtuple('Event','time proc action')
# 每辆出租车调用一次该函数,用于创建一个生成器对象,用来表示各辆出租车的运营过程。
# ident是出租车编号
# trips 是出租车回家之前的形成数量
def taxi_process(ident,trips, start_time = 0):
"""每次改变状态时创建事件,把控制权让给仿真器"""
# 离开停车场事件,执行到此,会暂停。当需要重新激活这个进程时,主循环会使用send方法发送当前的仿真事件赋值给time
time = yield Event(start_time,ident,'leave garage')
#每次行程都会执行一遍此处的代码块
for i in range(trips):
# 产生一个Event实例,表示拉到乘客了。协程在这里会暂停,需要激活时,主循环会使用send方法发送当前时间。
time = yield Event(time,ident,'pick up passenger')
time = yield Event(time,ident,'drop off passenger')
# 指定行程数量完成后,产生回家事件。此处,协程最后一次暂停。
yield Event(time,ident,'going home')
taxi = taxi_process(ident=12,trips=2,start_time=0)
next(taxi)
print(taxi.send( 7))
print(taxi.send(10))
print(taxi.send(15))
print(taxi.send(25))
print(taxi.send(35))
# 输出:
Event(time=7, proc=12, action='pick up passenger')
Event(time=10, proc=12, action='drop off passenger')
Event(time=15, proc=12, action='pick up passenger')
Event(time=25, proc=12, action='drop off passenger')
Event(time=35, proc=12, action='going home')