2.1.1.3 生成器

生成器

生成器是一个可以产生一个结果序列而不是单一值的函数。

David Beazley — 协程和并发的有趣课程

创建迭代器对应的第三种方法是调用生成器函数。生成器是包含关键字yield的函数。必须注意,只要这个关键词出现就会彻底改变函数的本质:这个yield关键字并不是必须激活或者甚至可到达,但是,会造成这个函数被标记为一个生成器。当普通函数被调用时,函数体内包含的指令就开始执行。当一个生成器被调用时,在函数体的第一条命令前停止执行。调用那个生成器函数创建一个生成器对象,继承迭代器协议。与调用普通函数一样,生成器也允许并发和递归。

next被调用时,函数执行到第一个yield。每一次遇到yield语句都会给出next的一个返回值。执行完yield语句,就暂停函数的执行。

In [20]:

  1. def f():
  2. yield 1
  3. yield 2
  4. f()

Out[20]:

  1. <generator object f at 0x105fbc460>

In [21]:

  1. gen = f()
  2. gen.next()

Out[21]:

  1. 1

In [22]:

  1. gen.next()

Out[22]:

  1. 2

In [23]:

  1. gen.next()
  1.  ------------- ------------- ------------- ------------- -----------------------
  2. StopIteration Traceback (most recent call last)
  3. <ipython-input-23-b2c61ce5e131> in <module>()
  4. ----> 1 gen.next()
  5. StopIteration:

让我们进入一次调用生成器函数的生命周期。

In [24]:

  1. def f():
  2. print("-- start --")
  3. yield 3
  4. print("-- middle --")
  5. yield 4
  6. print("-- finished --")
  7. gen = f()
  8. next(gen)
  1. -- start --

Out[24]:

  1. 3

In [25]:

  1. next(gen)
  1. -- middle --

Out[25]:

  1. 4

In [26]:

  1. next(gen)
  1. -- finished --
  1.  ------------- ------------- ------------- ------------- -----------------------
  2. StopIteration Traceback (most recent call last)
  3. <ipython-input-26-67c2d9ac4268> in <module>()
  4. ----> 1 next(gen)
  5. StopIteration:

与普通函数不同,当执行f()时会立即执行第一个print,函数赋值到gen没有执行函数体内的任何语句。只有当用next激活gen.next()时,截至到第一个yield的语句才会被执行。第二个next打印— middle —,执行到第二个yield终止。 第三个next打印— finished —,并且到达了函数末尾。因为没有找到yield,抛出异常。

当向调用者传递控制时,在yield之后函数内发生了什么?每一个生成器的状态被存储在生成器对象中。从生成器函数的角度,看起来几乎是在一个独立的线程运行,但是,这是一个假象:执行是非常严格的单线程,但是解释器记录并恢复next值请求间的状态。

为什么生成器有用?正如迭代器部分的提到的,生成器只是创建迭代对象的不同方式。用yield语句可以完成的所有事,也都可以用next方法完成。尽管如此,使用函数,并让解释器执行它的魔法来创建迭代器有优势。函数比定义一个带有nextiter方法的类短很多。更重要的是,理解在本地变量中的状态比理解实例属性的状态对于生成器的作者来说要简单的多,对于后者来事必须要在迭代对象上不断调用next

更广泛的问题是为什么迭代器有用?当迭代器被用于循环时,循环变的非常简单。初始化状态、决定循环是否结束以及寻找下一个值的代码被抽取到一个独立的的地方。这强调了循环体 - 有趣的部分。另外,这使在其他地方重用这些迭代体成为可能。