一. 让协程返回值
下面的例子,我们再次改版之前计算平均值的协程函数,这一版本的协程函数每次被激活时,不会自动产出平均值,而是在最后返回一个值。
from collections import namedtuple
Result = namedtuple('Result', 'count average')
def average():
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)
为了返回值,协程必须正常终止,因此 average
中有个判断条件,以便退出累计循环。返回值为 namedtuple
,包含 count
和 average
两个字段。
演示 1 发送 None
会终止循环,导致协程结束
注意:协程结束,协程对象会抛出 StopIteration
异常。return
表达式的值通过异常对象 StopIteration
传递给调用方。这样做有点不合常理,但是能保留生成器对象的常规行为,即在耗尽时抛出 StopIteration
异常。
演示 2 获取协程 return
的值
捕获 StopIteration
异常,并通过异常对象的 value
属性获取 average
返回的值。
下面我们介绍的 yield from
结构会在内部自动捕获 StopIteration
异常,这种处理方式与 for
循环处理 StopIteration
异常的方式一样:循环机制使用用户易于理解的方式处理异常。
对 yield from
结构来说,解释器不仅会捕获 StopIteration
异常,还会把 value
属性的值变为 yield from
表达式的值。
二. 使用 yield from
首先要明确,yield from
是全新的语法结构,其功能要比 yield
丰富。
演示 1 yield from
可用于简化 for
循环中的 yield
表达式
演示 2 使用 yield from
链接可迭代对象
yield from x
表达式对 x
对象所做的第一件事就是调用 iter(x)
,从中获取迭代器。因此 x
可以是任何可迭代的对象。
yield from
的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样二者可以直接发送和产出值,还可以直接传入异常,而不用在位于中间的协程中添加大量处理异常的样板代码。
在举例前,我们先来了解几个专门的术语:
- 委派生成器:包含
yield from <iterable>
表达式的生成器函数。 - 子生成器:从
yield from
表达式中<iterable>
部分获取的生成器。 - 调用方:调用委派生成器的客户端代码。在适当的语境中,将使用“客户端”代指“调用方”,以便于委派生成器(也是调用方,因为它调用了子生成器)区分开。
委派生成器在 yield from
表达式处暂停时,调用方可以直接把数据发给子生成器,子生成器再把产出的值发给调用方。子生成器返回之后,解释器会抛出 StopIteration
异常,并把返回的值附加到异常对象上,此时委派生成器会恢复。
示例 使用 yield from
计算平均值,并输出统计报告
from collections import namedtuple
Result = namedtuple('Result', 'count, average')
# 子生成器
def average():
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 average()
print(results[key])
# 客户端代码,即调用方
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)
# 输出报告
def report(results):
print('-'*20, 'report', '-'*20)
for key, result in sorted(results.items()):
group, unit = key.split(';')
print(f'{result.count:2} {group:5} averaging {result.average:.2f}{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], }
if __name__ == '__main__':
main(data)
运行结果:外层 for
循环每次迭代会产生一个 grouper
实例,赋值给 group
变量,group
是委派生成器。
调用 next(group)
,预激委派生成器 group
,此时进入 while
循环,调用子生成器 average
后,在 yield from
表达式处暂停。内层 for
循环调用 group.send(value)
,直接把值传给子生成器 average
。同时,当前的 group
在 yield from
表达式处暂停。
内层循环结束后,group
实例依旧在 yield from
表达式处暂停。因此,grouper
函数定义体中为 results[key]
赋值的语句还没有执行。如果外层 for
循环的末尾没有 group.send(None)
,那么 average
子生成器永远不会终止,委派生成器 group
永远不会再次激活,因此永远不会为 results[key]
赋值。
外层 for
循环重新迭代时会新建一个 grouper
实例,然后绑定到 group
变量上。前一个 grouper
实例,以及它创建的尚未终止的 average
子生成器实例,被垃圾回收机制回收。
注意:
-
group.send(None)
以及average
循环中的条件判断是至关重要的终止条件。如果不这么做,使用yield from
调用这个协程的生成器会永远阻塞。 - 返回的
Result
会成为grouper
函数中yield from
表达式的值。 -
grouper
是委派生成器; - 这个循环每次迭代时会新建一个
average
实例,每个实例都作为协程使用的生成器对象。 -
grouper
发送的每个值都会经由yield from
处理,通过管道传给average
实例。group
会在yield from
表达式处暂停,等待average
实例处理客户端发来的值。average
实例运行完毕后,返回的值绑定到results[key]
上。while
循环会不断创建average
实例,处理更多的值。 - 把各个
value
传给grouper
,传入的值最终到达averager
函数中term = yield
那一行;grouper
永远不知道传入的值是什么。 - 把
None
传入grouper
,导致当前的averager
实例终止,也让grouper
继续运行,再创建一个averager
实例,处理下一组值。
这个实验想表名的关键一点是,如果子生成器不终止,委派生成器会在 yield from
表达式处永远暂停。如果是这样,程序不会向前执行,因为 yield from
把控制权交给客户代码(即委派生成器的调用方)了。
yield from
的意义
- 子生成器产出的值都直接传给委派生成器的调用方。
- 使用
send
方法发给委派生成器的值都直接传给子生成器。如果发送的值是None
,那么会调用子生成器的__next__
方法。如果发送的值不是None
,那么会调用子生成器的send
方法。如果调用的方法抛出StopIteration
异常,那么委派生成器恢复运行。任何其它异常都会向上冒泡,传给委派生成器。 - 生成器退出时,生成器中的
return expr
表达式会触发StopIteration
异常抛出。 -
yield from
表达式的值是子生成器终止时传给StopIteration
异常的第一个参数。
关于 yield
实现生成器的介绍至此已经完成,下一节我们将使用 asyncio
来实现协程。