本文着重讨论《流畅的python》一书中第十六章第七小节中对yield from使用的示例代码及其疑似误导的注解。(个人认为试例代码的注解有些不明确,会使入门读者产生歧义且“不明觉厉”)
重要的事情再说一遍。。这是个人见解但给出了观点及其理由and这就是个希望发起讨论的文章
有关yield关键字及生成器与协程请见其他博客。
源代码见https://github.com/l65775622/example-code/blob/master/16-coroutine/coroaverager3.py(这里是分支代码)
下面进行yield from的介绍让后进入正题——对书中代码的讨论=。。=
作为python新的语言结构,yield from在PEP 380中的介绍为“把职责委托给子生成器的句法”,在《流畅的python》中的介绍为“yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样二者可以直接发送和去除值,还可以直接传入异常,而不用在位于中间的协程中添加大量处理异常的样板代码”,并引入专门术语:
委派生成器: 包含 yield from <iterable> 表达式的生成器函数,
子生成器: 从yield from 表达式中<iterable>部分获取的生成器,
调用方: 主函数之类的。。
(图片是手机照的。。这个不好画= =。 图片取自《流畅的python》P395)
下面进入正题。。
#《流畅的python》16-17示例代码
from collections import namedtuple
Result = namedtuple('Result', 'count average')
# 子生成器
def averager(): # <1>
total = 0.0
count = 0
average = None
while True:
term = yield # <2>
if term is None: # <3>
break
total += term
count += 1
average = total/count
return Result(count, average) # <4>
# 委派生成器
def grouper(results, key): # <5>
while True: # <6>
results[key] = yield from averager() # <7>
# 客户端代码(调用方)
def main(data): # <8>
results = {}
for key, values in data.items():
group = grouper(results, key) # <9>
next(group) # <10>
for value in values:
group.send(value) # <11>
group.send(None) # important! <12>
# print(results) # uncomment to debug
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)
在16-17示例代码中grouper函数作为委派生成器书中给出的代码注解
def grouper(results, key): # <5>
while True: # <6>
results[key] = yield from averager() # <7>
- 注解<6> 这个循环每次迭代时会新建一个averager实例;每个实例都是作为协程使用的生成器对象。
- 注解<7>grouper发送的每个值都会经由yield from处理,通过管道传给averager实例。grouper会在yield from表达式出暂停,等待averager实例处理客户端发来的值。averager实例运行完毕后,返回的值绑定到results[key]上。while循环会不断创建averager实例,处理更多的值。
但在其代码语境来看委派生成器<grouper>是由主函数中外层for循环处建立,每次建立一个委派生成器都是为了获取一个子生成器来处理一系列数据(此次循环中data[key]),很显然语境与注解给出的语义有些冲突,我在这里首次对<grouper>中while True的存在的意义产生怀疑(注解的不明确甚至给我个人感觉很有误导性),并进行了下面的测试and总结了个人观点。
- 直接去除while True并观察results值
def grouper(results, key): # <5> # while True: # <6> # results[key] = yield from averager() # <7> results[key] = yield from averager() # <7> print(results)
在测试结果中清晰地表明一个子生成器完成了它的任务计算出了data中第一项数据但由于某个生成器出现StopIteration异常使程序终止(协程中未处理的异常会向上冒泡所以目前不确定异常是由哪个生成器产出),在debug过程中发现主函数与子生成器的沟通都通过委派生成器<grouper>跳转且子生成器因主函数调用send(None)而抛出的StopIteration异常被yield from处理(yield from结构会在内部自动捕获stopiteration异常),由此推测致使程序终止的StopIteration异常由grouper产出。
-
使用一些方式替换while True保证输出、语义不变并做到最小的修改
def grouper(results, key): # <5> # 这里使用了yield使委托派成器在执行完yield from后进行让步 # 当然也可以在主函数外循环中使用try-except捕获异常 results[key] = yield from averager() # <7> yield
我们可以在Debugger中看到委派生成器在执行完每次计算任务(每次的主函数内循环)后、由主函数send(None)结束该任务并由子生成器返回本次任务计算数值给yield from(StopIteration.value)并赋值给results[key],这之后委派生成器将继续向下执行遇到yield关键字让步,等待调用方创建新的委派生成器处理任务。在上述描述中子生成器<averager>直接与调用方进行沟通并在一次任务的多次计算而并不是像注解<7>所说‘while循环会不断创建averager实例,处理更多的值。’,事实上在这个示例代码中一个委派生成器创建的除第一个averager外并没有发挥作用,在调用方新创建另一个委派生成器实例后上一个委派生成器及它所创建的尚未终止的子生成器都将被垃圾回收程序回收——它是多余的。在书中P397提醒到“外层for循环重新迭代时会创建一个grouper实例,然后绑定到group变量上。前一个grouper实例(以及它创建的尚未终止的averager子生成器实例)被垃圾回收程序回收”,这似乎在提醒读者示例代码grouper中有坑,但注解<6><7>曾在一段时间内一定程度上困惑着我。更改后输出无误
总结:虽然代码能够按照预期正确地执行任务,但我认为做为教学书籍《流畅的python》对这段示例代码的注解<6><7>确实有些描述的不够确切,容易对yield from结构的作用产生误解,但产生疑问后对这段代码进行分析、测试(debug)后不难理解其真正意义。
(本文仅供个人笔记及水友讨论用,,,)