- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- Python 术语表
- Python 版本表
- 排版约定
- 使用代码示例
- 第一部分 序幕
- 第 1 章 Python 数据模型
- 第二部分 数据结构
- 第 2 章 序列构成的数组
- 第 3 章 字典和集合
- 第 4 章 文本和字节序列
- 第三部分 把函数视作对象
- 第 5 章 一等函数
- 第 6 章 使用一等函数实现设计模式
- 第 7 章 函数装饰器和闭包
- 第四部分 面向对象惯用法
- 第 8 章 对象引用、可变性和垃圾回收
- 第 9 章 符合 Python 风格的对象
- 第 10 章 序列的修改、散列和切片
- 第 11 章 接口:从协议到抽象基类
- 第 12 章 继承的优缺点
- 第 13 章 正确重载运算符
- 第五部分 控制流程
- 第 14 章 可迭代的对象、迭代器和生成器
- 14.1 Sentence 类第1版:单词序列
- 14.2 可迭代的对象与迭代器的对比
- 14.3 Sentence 类第2版:典型的迭代器
- 14.4 Sentence 类第3版:生成器函数
- 14.5 Sentence 类第4版:惰性实现
- 14.6 Sentence 类第5版:生成器表达式
- 14.7 何时使用生成器表达式
- 14.8 另一个示例:等差数列生成器
- 14.9 标准库中的生成器函数
- 14.10 Python 3.3 中新出现的句法:yield from
- 14.11 可迭代的归约函数
- 14.12 深入分析 iter 函数
- 14.13 案例分析:在数据库转换工具中使用生成器
- 14.14 把生成器当成协程
- 14.15 本章小结
- 14.16 延伸阅读
- 第 15 章 上下文管理器和 else 块
- 第 16 章 协程
- 第 17 章 使用期物处理并发
- 第 18 章 使用 asyncio 包处理并发
- 第六部分 元编程
- 第 19 章 动态属性和特性
- 第 20 章 属性描述符
- 第 21 章 类元编程
- 结语
- 延伸阅读
- 附录 A 辅助脚本
- Python 术语表
- 作者简介
- 关于封面
16.6 让协程返回值
示例 16-13 是 averager 协程的不同版本,这一版会返回结果。为了说明如何返回值,每次激活协程时不会产出移动平均值。这么做是为了强调某些协程不会产出值,而是在最后返回一个值(通常是某种累计值)。
示例 16-13 中的 averager 协程返回的结果是一个 namedtuple,两个字段分别是项数(count)和平均值(average)。我本可以只返回平均值,但是返回一个元组可以获得累积数据的另一个重要信息——项数。
示例 16-13 coroaverager2.py:定义一个求平均值的协程,让它返回一个结果
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) ➋
➊ 为了返回值,协程必须正常终止;因此,这一版 averager 中有个条件判断,以便退出累计循环。
➋ 返回一个 namedtuple,包含 count 和 average 两个字段。在 Python 3.3 之前,如果生成器返回值,解释器会报句法错误。
下面在控制台中说明如何使用新版 averager,如示例 16-14 所示。
示例 16-14 coroaverager2.py:说明 averager 行为的 doctest
>>> coro_avg = averager() >>> next(coro_avg) >>> coro_avg.send(10) ➊ >>> coro_avg.send(30) >>> coro_avg.send(6.5) >>> coro_avg.send(None) ➋ Traceback (most recent call last): ... StopIteration: Result(count=3, average=15.5)
❶ 这一版不产出值。
❷ 发送 None 会终止循环,导致协程结束,返回结果。一如既往,生成器对象会抛出 StopIteration 异常。异常对象的 value 属性保存着返回的值。
注意,return 表达式的值会偷偷传给调用方,赋值给 StopIteration 异常的一个属性。这样做有点不合常理,但是能保留生成器对象的常规行为——耗尽时抛出 StopIteration 异常。
示例 16-15 展示如何获取协程返回的值。
示例 16-15 捕获 StopIteration 异常,获取 averager 返回的值
>>> coro_avg = averager() >>> next(coro_avg) >>> coro_avg.send(10) >>> coro_avg.send(30) >>> coro_avg.send(6.5) >>> try: ... coro_avg.send(None) ... except StopIteration as exc: ... result = exc.value ... >>> result Result(count=3, average=15.5)
获取协程的返回值虽然要绕个圈子,但这是 PEP 380 定义的方式,当我们意识到这一点之后就说得通了:yield from 结构会在内部自动捕获 StopIteration 异常。这种处理方式与 for 循环处理 StopIteration 异常的方式一样:循环机制使用用户易于理解的方式处理异常。对 yield from 结构来说,解释器不仅会捕获 StopIteration 异常,还会把 value 属性的值变成 yield from 表达式的值。可惜,我们无法在控制台中使用交互的方式测试这种行为,因为在函数外部使用 yield from(以及 yield)会导致句法出错。4
4iPython 有个扩展——ipython-yf,安装这个扩展后可以在 iPython 控制台中直接执行 yield from。这个扩展用于测试异步代码,可以结合 asyncio 模块使用。这个扩展已经提交为 Python 3.5 的补丁,但是没有被接受。参见 Python 缺陷追踪系统中的 22412 号工单: Towards an asyncio-enabled command line。
下一节会举例说明如何使用 yield from 结构按照 PEP 380 定义的方式获取 averager 协程返回的值。下面讨论 yield from 结构。
16.7 使用yield from
首先要知道,yield from 是全新的语言结构。它的作用比 yield 多很多,因此人们认为继续使用那个关键字多少会引起误解。在其他语言中,类似的结构使用 await 关键字,这个名称好多了,因为它传达了至关重要的一点:在生成器 gen 中使用 yield from subgen() 时,subgen 会获得控制权,把产出的值传给 gen 的调用方,即调用方可以直接控制 subgen。与此同时,gen 会阻塞,等待 subgen 终止。5
5写作本书时,有个 PEP 正在讨论中,提议增加 await 和 async 关键字:PEP 492—Coroutines with async and await syntax。
第 14 章说过,yield from 可用于简化 for 循环中的 yield 表达式。例如:
>>> def gen(): ... for c in 'AB': ... yield c ... for i in range(1, 3): ... yield i ... >>> list(gen()) ['A', 'B', 1, 2]
可以改写为:
>>> def gen(): ... yield from 'AB' ... yield from range(1, 3) ... >>> list(gen()) ['A', 'B', 1, 2]
14.10 节首次提到 yield from 时举了一个例子,演示这个结构的用法,如示例 16-16 所示。6
6示例 16-16 仅供教学使用。itertools 模块提供了优化版 chain 函数,使用 C 语言编写。
示例 16-16 使用 yield from 链接可迭代的对象
>>> def chain(*iterables): ... for it in iterables: ... yield from it ... >>> s = 'ABC' >>> t = tuple(range(3)) >>> list(chain(s, t)) ['A', 'B', 'C', 0, 1, 2]
在 Beazley 与 Jones 的《Python Cookbook(第 3 版)中文版》一书中,“4.14 扁平化处理嵌套型的序列”一节有个稍微复杂(不过更有用)的 yield from 示例(源码在 GitHub中)。
yield from x 表达式对 x 对象所做的第一件事是,调用 iter(x),从中获取迭代器。因此,x 可以是任何可迭代的对象。
可是,如果 yield from 结构唯一的作用是替代产出值的嵌套 for 循环,这个结构很有可能不会添加到 Python 语言中。yield from 结构的本质作用无法通过简单的可迭代对象说明,而要发散思维,使用嵌套的生成器。因此,引入 yield from 结构的 PEP 380 才起了“Syntax for Delegating to a Subgenerator”(“把职责委托给子生成器的句法”)这个标题。
yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样二者可以直接发送和产出值,还可以直接传入异常,而不用在位于中间的协程中添加大量处理异常的样板代码。有了这个结构,协程可以通过以前不可能的方式委托职责。
若想使用 yield from 结构,就要大幅改动代码。为了说明需要改动的部分,PEP 380 使用了一些专门的术语。
委派生成器
包含 yield from <iterable> 表达式的生成器函数。
子生成器
从 yield from 表达式中 <iterable> 部分获取的生成器。这就是 PEP 380 的标题(“Syntax for Delegating to a Subgenerator”)中所说的“子生成器”(subgenerator)。
调用方
PEP 380 使用“调用方”这个术语指代调用委派生成器的客户端代码。在不同的语境中,我会使用“客户端”代替“调用方”,以此与委派生成器(也是调用方,因为它调用了子生成器)区分开。
PEP 380 经常使用“迭代器”这个词指代子生成器。这样会让人误解,因为委派生成器也是迭代器。因此,我选择使用“子生成器”这个术语,与 PEP 380 的标题(“Syntax for Delegating to a Subgenerator”)保持一致。然而,子生成器可能是简单的迭代器,只实现了 __next__ 方法;但是,yield from 也能处理这种子生成器。不过,引入 yield from 结构的目的是为了支持实现了 __next__、send、close 和 throw 方法的生成器。
示例 16-17 能更好地说明 yield from 结构的用法。图 16-2 把该示例中各个相关的部分标识出来了。7
7图 16-2 的灵感来自 Paul Sokolovsky 绘制的示意图。
图 16-2:委派生成器在 yield from 表达式处暂停时,调用方可以直接把数据发给子生成器,子生成器再把产出的值发给调用方。子生成器返回之后,解释器会抛出 StopIteration 异常,并把返回值附加到异常对象上,此时委派生成器会恢复
coroaverager3.py 脚本从一个字典中读取虚构的七年级男女学生的体重和身高。例如, 'boys;m' 键对应于 9 个男学生的身高(单位是米),'girls;kg' 键对应于 10 个女学生的体重(单位是千克)。这个脚本把各组数据传给前面定义的 averager 协程,然后生成一个报告,如下所示:
$ python3 coroaverager3.py 9 boys averaging 40.42kg 9 boys averaging 1.39m 10 girls averaging 42.04kg 10 girls averaging 1.43m
示例 16-17 中列出的代码显然不是解决这个问题最简单的方案,但是通过实例说明了 yield from 结构的用法。这个示例的灵感来自“What's New in Python 3.3”一文给出的例子。
示例 16-17 coroaverager3.py:使用 yield from 计算平均值并输出统计报告
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 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) # 重要! ⓬ # 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)
❶ 与示例 16-13 中的 averager 协程一样。这里作为子生成器使用。
❷ main 函数中的客户代码发送的各个值绑定到这里的 term 变量上。
❸ 至关重要的终止条件。如果不这么做,使用 yield from 调用这个协程的生成器会永远阻塞。
❹ 返回的 Result 会成为 grouper 函数中 yield from 表达式的值。
❺ grouper 是委派生成器。
❻ 这个循环每次迭代时会新建一个 averager 实例;每个实例都是作为协程使用的生成器对象。
❼ grouper 发送的每个值都会经由 yield from 处理,通过管道传给 averager 实例。grouper 会在 yield from 表达式处暂停,等待 averager 实例处理客户端发来的值。averager 实例运行完毕后,返回的值绑定到 results[key] 上。while 循环会不断创建 averager 实例,处理更多的值。
❽ main 函数是客户端代码,用 PEP 380 定义的术语来说,是“调用方”。这是驱动一切的函数。
❾ group 是调用 grouper 函数得到的生成器对象,传给 grouper 函数的第一个参数是 results,用于收集结果;第二个参数是某个键。group 作为协程使用。
❿ 预激 group 协程。
⓫ 把各个 value 传给 grouper。传入的值最终到达 averager 函数中 term = yield 那一行;grouper 永远不知道传入的值是什么。
⓬ 把 None 传入 grouper,导致当前的 averager 实例终止,也让 grouper 继续运行,再创建一个 averager 实例,处理下一组值。
示例 16-17 中最后一个标号前面有个注释——“重要!”,强调这行代码(group.send(None))至关重要:终止当前的 averager 实例,开始执行下一个。如果注释掉那一行,这个脚本不会输出任何报告。此时,把 main 函数靠近末尾的 print(results) 那行的注释去掉,你会发现,results 字典是空的。
研究为何没有收集到数据,能检验自己有没有理解 yield from 结构的运作方式。本书的代码仓库中有 coroaverager3.py 脚本的代码。原因说明如下。
下面简要说明示例 16-17 的运作方式,还会说明把 main 函数中调用 group.send(None) 那一行代码(带有“重要!”注释的那一行)去掉会发生什么事。
外层 for 循环每次迭代会新建一个 grouper 实例,赋值给 group 变量;group 是委派生成器。
调用 next(group),预激委派生成器 grouper,此时进入 while True 循环,调用子生成器 averager 后,在 yield from 表达式处暂停。
内层 for 循环调用 group.send(value),直接把值传给子生成器 averager。同时,当前的 grouper 实例(group)在 yield from 表达式处暂停。
内层循环结束后,group 实例依旧在 yield from 表达式处暂停,因此,grouper 函数定义体中为 results[key] 赋值的语句还没有执行。
如果外层 for 循环的末尾没有 group.send(None),那么 averager 子生成器永远不会终止,委派生成器 group 永远不会再次激活,因此永远不会为 results[key] 赋值。
外层 for 循环重新迭代时会新建一个 grouper 实例,然后绑定到 group 变量上。前一个 grouper 实例(以及它创建的尚未终止的 averager 子生成器实例)被垃圾回收程序回收。
这个试验想表明的关键一点是,如果子生成器不终止,委派生成器会在 yield from 表达式处永远暂停。如果是这样,程序不会向前执行,因为 yield from(与 yield 一样)把控制权转交给客户代码(即,委派生成器的调用方)了。显然,肯定有任务无法完成。
示例 16-17 展示了 yield from 结构最简单的用法,只有一个委派生成器和一个子生成器。因为委派生成器相当于管道,所以可以把任意数量个委派生成器连接在一起:一个委派生成器使用 yield from 调用一个子生成器,而那个子生成器本身也是委派生成器,使用 yield from 调用另一个子生成器,以此类推。最终,这个链条要以一个只使用 yield 表达式的简单生成器结束;不过,也能以任何可迭代的对象结束,如示例 16-16 所示。
任何 yield from 链条都必须由客户驱动,在最外层委派生成器上调用 next(...) 函数或 .send(...) 方法。可以隐式调用,例如使用 for 循环。
下面综述 PEP 380 对 yield from 结构的正式说明。
16.8 yield from的意义
制定 PEP 380 时,有人质疑作者 Greg Ewing 提议的语义过于复杂了。他的回应之一是:“对人类来说,几乎所有最重要的信息都在靠近顶部的某个段落里。”他还引述了 PEP 380 草稿中的一段话,当时那段话是这样的:
“把迭代器当作生成器使用,相当于把子生成器的定义体内联在 yield from 表达式中。此外,子生成器可以执行 return 语句,返回一个值,而返回的值会成为 yield from 表达式的值。”8
8摘自 Python-Dev 邮件列表中的一个消息:“PEP 380 (yield from a subgenerator) comments”(发布于 2009 年 3 月 21 日)。
PEP 380 中已经没有这段宽慰人心的话,因为没有涵盖所有极端情况。不过,一开始可以这样粗略地说。
批准后的 PEP 380 在“Proposal”一节分六点说明了 yield from 的行为。这里,我几乎原封不动地引述,不过把有歧义的“迭代器”一词都换成了“子生成器”,还做了进一步说明。示例 16-17 阐明了下述四点。
子生成器产出的值都直接传给委派生成器的调用方(即客户端代码)。
使用 send() 方法发给委派生成器的值都直接传给子生成器。如果发送的值是 None,那么会调用子生成器的 __next__() 方法。如果发送的值不是 None,那么会调用子生成器的 send() 方法。如果调用的方法抛出 StopIteration 异常,那么委派生成器恢复运行。任何其他异常都会向上冒泡,传给委派生成器。
生成器退出时,生成器(或子生成器)中的 return expr 表达式会触发 StopIteration(expr) 异常抛出。
yield from 表达式的值是子生成器终止时传给 StopIteration 异常的第一个参数。
yield from 结构的另外两个特性与异常和终止有关。
传入委派生成器的异常,除了 GeneratorExit 之外都传给子生成器的 throw() 方法。如果调用 throw() 方法时抛出 StopIteration 异常,委派生成器恢复运行。StopIteration 之外的异常会向上冒泡,传给委派生成器。
如果把 GeneratorExit 异常传入委派生成器,或者在委派生成器上调用 close() 方法,那么在子生成器上调用 close() 方法,如果它有的话。如果调用 close() 方法导致异常抛出,那么异常会向上冒泡,传给委派生成器;否则,委派生成器抛出 GeneratorExit 异常。
yield from 的具体语义很难理解,尤其是处理异常的那两点。Greg Ewing 做得很好,在 PEP 380 中使用英语阐述了 yield from 的语义。
Ewing 还使用伪代码(使用 Python 句法)演示了 yield from 的行为。我个人认为值得花时间研究 PEP 380 中的伪代码。不过,那段伪代码长达 40 行,看一遍很难理解。
若想研究那段伪代码,最好将其简化,只涵盖 yield from 最基本且最常见的用法。
假设 yield from 出现在委派生成器中。客户端代码驱动着委派生成器,而委派生成器驱动着子生成器。那么,为了简化涉及到的逻辑,我们假设客户端没有在委派生成器上调用 .throw(...) 或 .close() 方法。此外,我们还假设子生成器不会抛出异常,而是一直运行到终止,让解释器抛出 StopIteration 异常。
示例 16-17 中的脚本就做了这些简化逻辑的假设。其实,在真实的代码中,委派生成器应该运行到结束。下面来看一下在这个简化的美满世界中,yield from 是如何运作的。
请看示例 16-18,那里列出的代码是委派生成器的定义体中下面这一行代码的扩充:
RESULT = yield from EXPR
自己试着理解示例 16-18 中的逻辑。
示例 16-18 简化的伪代码,等效于委派生成器中的 RESULT = yield from EXPR 语句(这里针对的是最简单的情况:不支持 .throw(...) 和 .close() 方法,而且只处理 StopIteration 异常)
_i = iter(EXPR) ➊ try: _y = next(_i) ➋ except StopIteration as _e: _r = _e.value ➌ else: while 1: ➍ _s = yield _y ➎ try: _y = _i.send(_s) ➏ except StopIteration as _e: ➐ _r = _e.value break RESULT = _r ➑
❶ EXPR 可以是任何可迭代的对象,因为获取迭代器 _i(这是子生成器)使用的是 iter() 函数。
❷ 预激子生成器;结果保存在 _y 中,作为产出的第一个值。
❸ 如果抛出 StopIteration 异常,获取异常对象的 value 属性,赋值给 _r——这是最简单情况下的返回值(RESULT)。
❹ 运行这个循环时,委派生成器会阻塞,只作为调用方和子生成器之间的通道。
❺ 产出子生成器当前产出的元素;等待调用方发送 _s 中保存的值。注意,这个代码清单中只有这一个 yield 表达式。
❻ 尝试让子生成器向前执行,转发调用方发送的 _s。
❼ 如果子生成器抛出 StopIteration 异常,获取 value 属性的值,赋值给 _r,然后退出循环,让委派生成器恢复运行。
❽ 返回的结果(RESULT)是 _r,即整个 yield from 表达式的值。
在这段简化的伪代码中,我保留了 PEP 380 中那段伪代码使用的变量名称。这些变量是:
_i(迭代器)
子生成器
_y(产出的值)
子生成器产出的值
_r(结果)
最终的结果(即子生成器运行结束后 yield from 表达式的值)
_s(发送的值)
调用方发给委派生成器的值,这个值会转发给子生成器
_e(异常)
异常对象(在这段简化的伪代码中始终是 StopIteration 实例)
除了没有处理 .throw(...) 和 .close() 方法之外,这段简化的伪代码还在子生成器上调用 .send(...) 方法,以此达到客户调用 next() 函数或 .send(...) 方法的目的。首次阅读时不要担心这些细微的差别。前面说过,即使 yield from 结构只做示例 16-18 中展示的事情,示例 16-17 也依旧能正常运行。
但是,现实情况要复杂一些,因为要处理客户对 .throw(...) 和 .close() 方法的调用,而这两个方法执行的操作必须传入子生成器。此外,子生成器可能只是纯粹的迭代器,不支持 .throw(...) 和 .close() 方法,因此 yield from 结构的逻辑必须处理这种情况。如果子生成器实现了这两个方法,而在子生成器内部,这两个方法都会触发异常抛出,这种情况也必须由 yield from 机制处理。调用方可能会无缘无故地让子生成器自己抛出异常,实现 yield from 结构时也必须处理这种情况。最后,为了优化,如果调用方调用 next(...) 函数或 .send(None) 方法,都要转交职责,在子生成器上调用 next(...) 函数;仅当调用方发送的值不是 None 时,才使用子生成器的 .send(...) 方法。
为了方便对比,下面列出 PEP 380 中扩充 yield from 表达式的完整伪代码,而且加上了带标号的注解。示例 16-19 中的代码是一字不差复制过来的,只有标注是我自己加的。
再次说明,示例 16-19 中的代码是委派生成器的定义体中下面这一个语句的扩充:
RESULT = yield from EXPR
示例 16-19 伪代码,等效于委派生成器中的 RESULT = yield from EXPR 语句
_i = iter(EXPR) ➊ try: _y = next(_i) ➋ except StopIteration as _e: _r = _e.value ➌ else: while 1: ➍ try: _s = yield _y ➎ except GeneratorExit as _e: ➏ try: _m = _i.close except AttributeError: pass else: _m() raise _e except BaseException as _e: ➐ _x = sys.exc_info() try: _m = _i.throw except AttributeError: raise _e else: ➑ try: _y = _m(*_x) except StopIteration as _e: _r = _e.value break else: ➒ try: ➓ if _s is None: ⓫ _y = next(_i) else: _y = _i.send(_s) except StopIteration as _e: ⓬ _r = _e.value break RESULT = _r ⓭
❶ EXPR 可以是任何可迭代的对象,因为获取迭代器 _i(这是子生成器)使用的是 iter() 函数。
❷ 预激子生成器;结果保存在 _y 中,作为产出的第一个值。
❸ 如果抛出 StopIteration 异常,获取异常对象的 value 属性,赋值给 _r——这是最简单情况下的返回值(RESULT)。
❹ 运行这个循环时,委派生成器会阻塞,只作为调用方和子生成器之间的通道。
❺ 产出子生成器当前产出的元素;等待调用方发送 _s 中保存的值。这个代码清单中只有这一个 yield 表达式。
❻ 这一部分用于关闭委派生成器和子生成器。因为子生成器可以是任何可迭代的对象,所以可能没有 close 方法。
❼ 这一部分处理调用方通过 .throw(...) 方法传入的异常。同样,子生成器可以是迭代器,从而没有 throw 方法可调用——这种情况会导致委派生成器抛出异常。
❽ 如果子生成器有 throw 方法,调用它并传入调用方发来的异常。子生成器可能会处理传入的异常(然后继续循环);可能抛出 StopIteration 异常(从中获取结果,赋值给 _r,循环结束);还可能不处理,而是抛出相同的或不同的异常,向上冒泡,传给委派生成器。
❾ 如果产出值时没有异常……
❿ 尝试让子生成器向前执行……
⓫ 如果调用方最后发送的值是 None,在子生成器上调用 next 函数,否则调用 send 方法。
⓬ 如果子生成器抛出 StopIteration 异常,获取 value 属性的值,赋值给 _r,然后退出循环,让委派生成器恢复运行。
⓭ 返回的结果(RESULT)是 _r,即整个 yield from 表达式的值。
这段 yield from 伪代码的大多数逻辑通过六个 try/except 块实现,而且嵌套了四层,因此有点难以阅读。此外,用到的其他流程控制关键字有一个 while、一个 if 和一个 yield。找到 while 循环、yield 表达式以及 next(...) 函数和 .send(...) 方法调用,这些代码有助于对 yield from 结构的运作方式有个整体的了解。
就在示例 16-19 所列伪代码的顶部,有行代码(标号❷)揭示了一个重要的细节:要预激子生成器。9 这表明,用于自动预激的装饰器(如 16.4 节定义的那个)与 yield from 结构不兼容。
9Nick Coghlan 于 2009 年 4 月 5 日在 Python-ideas 邮件列表中发布的一个消息中质疑,yield from 结构隐式预激是不是好主意。
在本节开头引用的那个消息中,关于扩充 yield from 结构的伪代码,Greg Ewing 说:
我不是让你通过扩充的伪代码学习这个结构,那段伪代码是为了让语言专家弄明白细节。
仔细研究扩充的伪代码可能没什么用——这与你的学习方式有关。显然,分析真正使用 yield from 结构的代码要比深入研究实现这一结构的伪代码更有好处。不过,我见过的 yield from 示例几乎都使用 asyncio 模块做异步编程,因此要有有效的事件循环才能运行。第 18 章会多次用到 yield from 结构。16.11 节中有几个链接,指向使用 yield from 结构的一些有趣代码,而且无需事件循环。
下面分析一个使用协程的经典案例:仿真编程。这个案例没有展示 yield from 结构的用法,但是揭示了如何使用协程在单个线程中管理并发活动。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论