python 生成器-协程示例代码的问题与讨论:yield、yield from、send

本文探讨了《流畅的Python》中关于yield from的示例代码,指出其注释可能导致对协程和生成器理解的混淆。作者通过测试和分析发现,while True循环在示例中的作用被错误解读,实际是每个grouper生成器仅创建一个averager子生成器,并在完成任务后被回收。文章建议改进注释以避免误导,并强调了生成器和调用方之间的交互机制。
摘要由CSDN通过智能技术生成

本文着重讨论《流畅的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总结了个人观点。

  1. 直接去除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产出。

  2. 使用一些方式替换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)后不难理解其真正意义。

(本文仅供个人笔记及水友讨论用,,,)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值