- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- 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 术语表
- 作者简介
- 关于封面
14.4 Sentence 类第3版:生成器函数
实现相同功能,但却符合 Python 习惯的方式是,用生成器函数代替 SentenceIterator 类。先看示例 14-5,然后详细说明生成器函数。
示例 14-5 sentence_gen.py:使用生成器函数实现 Sentence 类
import re import reprlib RE_WORD = re.compile('\w+') class Sentence: def __init__(self, text): self.text = text self.words = RE_WORD.findall(text) def __repr__(self): return 'Sentence(%s)' % reprlib.repr(self.text) def __iter__(self): for word in self.words: ➊ yield word ➋ return ➌ # 完成! ➍
❶ 迭代 self.words。
❷ 产出当前的 word。
❸ 这个 return 语句不是必要的;这个函数可以直接“落空”,自动返回。不管有没有 return 语句,生成器函数都不会抛出 StopIteration 异常,而是在生成完全部值之后会直接退出。6
6Alex Martelli 审查这段代码时建议简化这个方法的定义体,直接使用 return iter(self.words)。当然,他是对的,毕竟调用 __iter__ 方法得到的就是迭代器。不过,这里我用的是 for 循环,而且用到了 yield 关键字,这样做是为了介绍生成器函数的句法。下一节会详细说明。
❹ 不用再单独定义一个迭代器类!
我们又使用一种不同的方式实现了 Sentence 类,而且也能通过示例 14-2 中的测试。
在示例 14-4 定义的 Sentence 类中,__iter__ 方法调用 SentenceIterator 类的构造方法创建一个迭代器并将其返回。而在示例 14-5 中,迭代器其实是生成器对象,每次调用 __iter__ 方法都会自动创建,因为这里的 __iter__ 方法是生成器函数。
下面全面说明生成器函数。
生成器函数的工作原理
只要 Python 函数的定义体中有 yield 关键字,该函数就是生成器函数。调用生成器函数时,会返回一个生成器对象。也就是说,生成器函数是生成器工厂。
普通的函数与生成器函数在句法上唯一的区别是,在后者的定义体中有 yield 关键字。有些人认为定义生成器函数应该使用一个新的关键字,例如 gen,而不该使用 def,但是 Guido 不同意。他的理由参见“PEP 255—Simple Generators”。7
7有时,我会在生成器函数的名称中加上 gen 前缀或后缀,不过这不是习惯做法。显然,如果实现的是迭代器,那就不能这么做,因为所需的特殊方法必须命名为 __iter__。
下面以一个特别简单的函数说明生成器的行为:8
8感谢 David Kwast 建议使用这个示例。
>>> def gen_123(): # ➊ ... yield 1 # ➋ ... yield 2 ... yield 3 ... >>> gen_123 # doctest: +ELLIPSIS <function gen_123 at 0x...> # ➌ >>> gen_123() # doctest: +ELLIPSIS <generator object gen_123 at 0x...> # ➍ >>> for i in gen_123(): # ➎ ... print(i) 1 2 3 >>> g = gen_123() # ➏ >>> next(g) # ➐ 1 >>> next(g) 2 >>> next(g) 3 >>> next(g) # ➑ Traceback (most recent call last): ... StopIteration
❶ 只要 Python 函数中包含关键字 yield,该函数就是生成器函数。
❷ 生成器函数的定义体中通常都有循环,不过这不是必要条件;这里我重复使用 3 次 yield。
❸ 仔细看,gen_123 是函数对象。
❹ 但是调用时,gen_123() 返回一个生成器对象。
❺ 生成器是迭代器,会生成传给 yield 关键字的表达式的值。
❻ 为了仔细检查,我们把生成器对象赋值给 g。
❼ 因为 g 是迭代器,所以调用 next(g) 会获取 yield 生成的下一个元素。
❽ 生成器函数的定义体执行完毕后,生成器对象会抛出 StopIteration 异常。
生成器函数会创建一个生成器对象,包装生成器函数的定义体。把生成器传给 next(...) 函数时,生成器函数会向前,执行函数定义体中的下一个 yield 语句,返回产出的值,并在函数定义体的当前位置暂停。最终,函数的定义体返回时,外层的生成器对象会抛出 StopIteration 异常——这一点与迭代器协议一致。
我觉得,使用准确的词语描述从生成器中获取结果的过程,有助于理解生成器。注意,我说的是产出或生成值。如果说生成器“返回”值,就会让人难以理解。函数返回值;调用生成器函数返回生成器;生成器产出或生成值。生成器不会以常规的方式“返回”值:生成器函数定义体中的 return 语句会触发生成器对象抛出 StopIteration 异常。9
9在 Python 3.3 之前,如果生成器函数中的 return 语句有返回值,那么会报错。现在可以这么做,不过 return 语句仍会导致 StopIteration 异常抛出。调用方可以从异常对象中获取返回值。可是,只有把生成器函数当成协程使用时,这么做才有意义,详情参见 16.6 节。
示例 14-6 使用 for 循环更清楚地说明了生成器函数定义体的执行过程。
示例 14-6 运行时打印消息的生成器函数
>>> def gen_AB(): # ➊ ... print('start') ... yield 'A' # ➋ ... print('continue') ... yield 'B' # ➌ ... print('end.') # ➍ ... >>> for c in gen_AB(): # ➎ ... print('-->', c) # ➏ ... start ➐ --> A ➑ continue ➒ --> B ➓ end. ⓫ >>> ⓬
❶ 定义生成器函数的方式与普通的函数无异,只不过要使用 yield 关键字。
❷ 在 for 循环中第一次隐式调用 next() 函数时(序号➎),会打印 'start',然后停在第一个 yield 语句,生成值 'A'。
❸ 在 for 循环中第二次隐式调用 next() 函数时,会打印 'continue',然后停在第二个 yield 语句,生成值 'B'。
❹ 第三次调用 next() 函数时,会打印 'end.',然后到达函数定义体的末尾,导致生成器对象抛出 StopIteration 异常。
❺ 迭代时,for 机制的作用与 g = iter(gen_AB()) 一样,用于获取生成器对象,然后每次迭代时调用 next(g)。
❻ 循环块打印 --> 和 next(g) 返回的值。但是,生成器函数中的 print 函数输出结果之后才会看到这个输出。
❼ 'start' 是生成器函数定义体中 print('start') 输出的结果。
❽ 生成器函数定义体中的 yield 'A' 语句会生成值 A,提供给 for 循环使用,而 A 会赋值给变量 c,最终输出 --> A。
❾ 第二次调用 next(g),继续迭代,生成器函数定义体中的代码由 yield 'A' 前进到 yield 'B'。文本 continue 是由生成器函数定义体中的第二个 print 函数输出的。
❿ yield 'B' 语句生成值 B,提供给 for 循环使用,而 B 会赋值给变量 c,所以循环打印出 --> B。
⓫ 第三次调用 next(it),继续迭代,前进到生成器函数的末尾。文本 end. 是由生成器函数定义体中的第三个 print 函数输出的。到达生成器函数定义体的末尾时,生成器对象抛出 StopIteration 异常。for 机制会捕获异常,因此循环终止时没有报错。
⓬ 现在,希望你已经知道示例 14-5 中 Sentence.__iter__ 方法的作用了:__iter__ 方法是生成器函数,调用时会构建一个实现了迭代器接口的生成器对象,因此不用再定义 SentenceIterator 类了。
这一版 Sentence 类比前一版简短多了,但是还不够懒惰。如今,人们认为惰性是好的特质,至少在编程语言和 API 中是如此。惰性实现是指尽可能延后生成值。这样做能节省内存,而且或许还可以避免做无用的处理。
下一节以这种惰性方式定义 Sentence 类。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论