代码引自《流畅的Python》16.7 使用yield from 一节
问题
为什么书中的案例里最后没有按预期收集到数据?
原因
因为子生成将return语句写到了循环内,所以调用方每次send()值到子生成器都会返回Result对象到委派生成器grouper,导致结果覆盖。
而最后一次send(None)触发子生成器退出循环时,又相当于子生成器做了一次return None,将委派生成器中结果进行覆盖,所以最终收集到的数据每个键的值都是None
底层机制
子生成器每次执行return语句后都会被释放,此时拥有程序控制权的委托生成器会再次生成一个子生成器,并走到yield语句处,等待调用方发送数据
修改方法
将子生成器的return语句放到循环之外,那么知道调用方send(None)时,才会返回最终的Result对象,最终收集到的结果正常
from collections import namedtuple
Result = namedtuple('Result', 'count average')
# 子生成器
def averager():
total = 0.0
count = 0
average = None
while True:
term = yield # main函数中的客户代码发送的各个值绑定到这里的term变量上
if term is None: # 至关重要的终止条件。如果不这么做,使用yield from调用这个协程的生成器会永远阻塞
break
total += term
count += 1
average = total / count
# return Result(count, average) *更改处*
return Result(count, average) # 返回的result会成为grouper函数中yield from表达式的值
# 委派生成器
def grouper(results, key):
while True: # 这个循环每次迭代时会新建一个averager实例,每个实例都是作为协程使用的生成器对象
"""
grouper发送的每个值都会经由yield from处理,通过管道传给averager实例。
grouper会在yield from表达式处暂停,等待averager实例处理客户端发来的值。
averager实例运行完毕后,返回的值绑定到results[key]上。
while循环会不断创建averager实例,处理更多的值。
"""
results[key] = yield from averager()
# 客户端代码,即调用方
def main(data):
results = {}
for key, values in data.items():
"""
group是调用grouper函数得到的生成器对象。
传给grouper函数的第一个参数results用于收集结果。
第二个参数是某个键,group作为协程使用
"""
group = grouper(results, key)
next(group) # 预激group协程
for value in values:
"""
把各个value传给grouper。传入的值最终到达averager函数中term = yield那一行;
grouper永远不知道传入的值是什么
"""
group.send(value)
"""
把None传入grouper,导致当前的averager实例终止,
也让grouper继续运行,再创建一个averager实例,处理下一组值
"""
group.send(None)
print(results)
report(results)
# 输出报告
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]
}
if __name__ == '__main__':
main(data)