协程
与生成器相比,协程中也运用到了yield关键字,但具备比生成器更多的功能。
协程的基本用法
下面通过一个例子描述协程的基本用法。
def simple_coro(a):
print('started: a = ', a)
b = yield a
print('recieved: b = ', b)
c = yield a + b
print('received: c = ', c)
coro = simple_coro(14)
print(inspect.getgeneratorstate(coro))
print(next(coro))
print(inspect.getgeneratorstate(coro))
print(coro.send(20))
try:
coro.send(15)
except StopIteration:
print(inspect.getgeneratorstate(coro))
"""output
GEN_CREATED
started: a = 14
14
GEN_SUSPENDED
recieved: b = 20
34
received: c = 15
GEN_CLOSED
"""
在协程中,yield右侧的用法和生成器类似,区别yield左侧=的赋值。
协程的状态
协程具有四种状态分别为:
- GEN_CREATED
- GEN_RUNNING
- GEN_SUSPENDED
- GEN_CLOSED
当调用simple_coro2(14)函数后,我们得到了协程coro。此时coro的状态为’GEN_CREATED’(可以通过标准库中的inspect.getgeneratorstate(coro)得到该状态)。
在执行next(coro)后,simple_coro函数开始执行(此时coro状态为GEN_RUNNING),打印出’started: a = 14’,直到遇到第一个yield暂时挂起,此时coro状态为GEN_SUSPENDED,在yield a处等待下一次调用者触发协程。这里的next(coro)类似,将获取到协程中yield a中的a值(14)。
在执行coro.send(20)时,coro的状态从GEN_SUSPENDED改变为GEN_RUNNING,并将send函数中的参数20赋值到yield左边的变量b,因此程序会打印出’recieved: b = 20’。协程继续运行,直到yield a + b, 将a + b的值返回给调用者,因此’coro.send(20)‘的值为34。
此时协程coro在yield a + b处暂时停止,如再次调用’coro.send(15)’,coro中的变量c将会接收这个值,coro此时运行到结尾,会抛出StopIteration的异常, 此时协程coro的状态则为GEN_CLOSED。
协程的关闭与抛出异常
如果协程在运行中遇到没有被try/except捕获的异常,协程将立即停止进入GEN_CLOSED状态,并将该异常传回调用者(send或next)。
import inspect
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))
try:
coro_avg.send('33')
except Exception as e:
print(e)
print(inspect.getgeneratorstate(coro_avg))
"""output
10.0
15.0
20.0
unsupported operand type(s) for +=: 'float' and 'str'
GEN_CLOSED
"""
协程由于接受了字符串’33’,在进行’total += term’语句时触发异常,由于在协程中没有捕获该异常,则将该异常传回send,我们在send处捕获该异常并打印出来。此时,协程coro_avg已经停止。
协程的close()与throw()
协程中有两种可以从调用方发送给协程的异常:
- generator.throw(exc_type[, exc_value[, traceback]])
调用方向协程发送异常,协程会在等待中的yield处抛出异常,如果该异常在该yield处被捕获,协程会继续运行,generator.throw的返回值则是下一个yield后的值。如果该异常未被捕获,协程将立即停止,将异常传递回generator.throw。
import inspect
class DemoException(Exception):
pass
def demo_exc_handling():
print('coroutine started')
while True:
try:
x = yield 200
except DemoException:
print('DemoException handled. Continuing...')
else:
print('coroutine received: {!r}'.format(x))
coro_exc = demo_exc_handling()
next(coro_exc)
coro_exc.send('hey')
print(coro_exc.throw(DemoException))
print(inspect.getgeneratorstate(coro_exc))
try:
coro_exc.throw(ZeroDivisionError, 500)
except ZeroDivisionError as e:
print(e)
print(inspect.getgeneratorstate(coro_exc))
"""output
coroutine started
coroutine received: 'hey'
DemoException handled. Continuing...
200
GEN_SUSPENDED
500
GEN_CLOSED
"""
- generator.close()
协程会在等待中的yield处抛出GeneratorExit异常,如果在协程中没有捕获该异常,协程将立刻关闭进入到GEN_CLOSED状态,而且GeneratorExit也不会传递到调用方的generator.close函数。当在协程中捕获GeneratorExit异常时,协程将继续运行如果此时抛出StopIteration异常或是再次执行yield语句时将会抛出RuntimeError异常给调用方,如果在捕获GeneratorExit后抛出其他类型的异常,该异常将会传递到调用方,并且协程也会立刻停止。
协程的返回值
在Python3.3之后,在协程中允许有返回值(Return)。
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 average
coro_avg = averager()
next(coro_avg)
coro_avg.send(10)
coro_avg.send(20)
coro_avg.send(30)
try:
coro_avg.send(None)
except StopIteration as e:
print(e.value)
"""output
20.0
"""
调用方通过捕获协程正常结束后抛出的StopIteration的value值得到协程Return的值,因此如果让协程关闭或是抛出异常(调用close或是throw函数),则协程会提前抛出所对应的异常,不会执行Return的操作。
yield from
yield from也同样是Python3.3之后新加入的语法,yield from具备处理嵌套生成器的相关功能,Python3中的迭代器和生成器。
除此之外,yield from更像是一种代理协程,它存在于调用方和yield from后的协程,它可以把调用方的close throw send等直接传递到里层的协程中,并可以处理里层协程抛出的异常返回值等,它就像是调用方和里层协程中的一种通道道。
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)
def grouper(results, key):
while True:
results[key] = yield from averager()
def report(results):
for key, result in sorted(results.items()):
group, unit = key.split(';')
print('{:2} {:5} averaging {:.2f}{}'.format(
result.count, group, result.average, unit))
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], }
def main(data):
results = {}
for key, values in data.items():
group = grouper(results, key)
next(group)
for value in values:
group.send(value)
group.send(None)
report(results)
main(data)
"""output
9 boys averaging 40.42kg
9 boys averaging 1.39m
10 girls averaging 42.04kg
10 girls averaging 1.43m
"""
在上述例子中,main作为调用方,average为里层的协程,grouper作为为中间的main和average中间的协程代理,可以传递send、yield等。
yield from的实现
摘自Fluent Python