返回介绍

14.4 Sentence 类第3版:生成器函数

发布于 2024-02-05 21:59:47 字数 5086 浏览 0 评论 0 收藏 0

实现相同功能,但却符合 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文