理解 Python 的生成器

考虑生产者/消费者问题,我们希望生产者能源源不断产生新的值(数据)给消费者使用,我们还希望生产者能够维持它所产生的值的状态,这样每次生产者都可以根据前面的信息(状态)产生新的值。Python 的生成器就为这种问题提供了一个很好的解决方法。

生成器是一种特殊的函数,其思想可以被简化成:为生成器的调用者提供一种函数,该函数每次可以返回一个中间结果(即,下一个值),然后暂停并且维护内部状态。这样,下一次调用该函数时,就可以从上次暂停的地方继续进行了。(因为生成器也是函数,所以我们也可以使用函数来指代生成器)

为了达到这样的目的:即返回一个中间结果同时暂停函数,保存内部状态,当然不能使用普通的 return 语句。因为 return 语句将会返回结果并且退出函数,所以 Python 引入了一个新的关键字 yield 来完成上述任务。yield 语句的定义是这样的:

yield 表达式

当函数在内部遇到 yield 语句,就会将表达式的值返回给函数的调用者,并且保存内部状态,暂停执行。下一次调用该函数时,就可以从上次暂停的地方继续执行。任何函数只要包含了 yield 语句,那么它就不再是普通的函数而是变成了生成器。

下面是利用生成器实现斐波那契数列的一个简单例子:

>>> def fib():
...    a, b = 0, 1
...    while True:
...        yield b
...        a, b = b, a + b

当你调用上述函数时,函数体并不会立即执行,而是返回一个生成器对象(这里,生成器既可以指生成器函数,即 fib() 函数;又可以指生成器对象,如 f):

>>> f = fib()
>>> f
<generator object fib at 0x7fb214117938>

要让上述生成器执行,需要调用 next() 方法:

>>> next(f)
1
>>> next(f)
1
>>> next(f)
2

当然,我们也可以使用 for 循环迭代生成器,类似迭代器一样。如下,打印小于100的斐波那契数:

>>> f=fib()
>>> for i in f:
...     if i>100:
...         break
...     else:
...         print(i)
1
1
2
3
5
8
13
21
34
55
89

事实上,当第一次调用 next() 方法时,函数体内部才开始执行,如上面函数,当第一次使用 next(),函数 fib 内部将 a 设置为 0,b 设置为 1,然后开始执行循环。

生成器函数内部也可以包含 return 语句。所以在函数内部,执行情况可以分三种:碰到 yield 语句,return 语句或者执行到了函数尾。其他情况下,函数将会正常执行。

  • 当函数碰到 yield 语句,就会将 yield 后面的表达式的值返回给 next() 的调用者,然后保存内部状态,暂停执行。如上面,当函数执行到 yield b 时,将 b 的值返回给 next(f) 调用者, 所以 next(f) 得到了 b = 1,同时 f 内部暂停执行。当下一次使用 next(f),f 将会从 yield b 处进行执行,即执行 a, b = b, a + b,然后继续进行循环直到再次碰到 yield b,这时过程类似上面。

  • 如果函数碰到 return 语句,那么这时 return 语句的作用就和普通函数类似,将使得生成器退出执行,只不过退出方式不同。如果有 finally 语句部分,那么生成器将会执行这部分。然后引发 StopIteration 异常,表示生成器停止了。

  • 类似地,当函数执行了函数体尾而没有显示的 return 语句,那么这时也视为函数将停止,引发 StopIteration 异常。

使用 return 结束生成器时,注意:在 Python2 中,return 后面不可以跟表达式(即使是 None 也不行),否则会报 SyntaxError。而在 Python3,则允许这种情况。即使是这样,我们不能按照正常方法获得该表达式的值。因为,当生成器执行到 return 语句,并不是正常返回,而是引发 StopIteration 异常,这时要获得该表达式的值,应该捕获异常,如:

>>> def test():
...     i = 0
...     while i < 2:
...         yield i
...         i += 1
...     return 'I am the returned value'
>>> t = test()
>>> next(t)
0
>>> next(t)
1
>>> try:
...    next(t)
... except StopIteration as e:
...    print(e.value)
I am the returned value

虽然 return 语句也是通过引发 StopIteration 异常来停止生成器,但是,return 和 StopIteration 异常并不总是相等的,这点可以体现在 try/except 结构上:

>>> def f1():
...     try:
...         return
...     except:
...        yield 1
>>> print list(f1())
[]

在上面例子中,return 正常返回,except 部分没有被执行,所以函数结果为空。而在下面的例子中:

>>> def f2():
...     try:
...         raise StopIteration
...     except:
...         yield 42
>>> print list(f2())
[42]

因为函数 f2() 内部引发了 StopIteration 异常,被 except 语句捕获,导致 yield 42 语句被执行,所以函数的结果为 42。

另外要注意的是,如果生成器内部引发了异常,包括但不限于 StopIteration 异常,而生成器没有捕获该异常。那么该异常就会传递给生成器的调用者,同时生成器将会停止。如果生成器停止了,那么后续使用 next() 方法调用生成器时就会得到 StopIteration 异常。如下:

>>> def f():
...     return 1/0
>>> def g():
...     yield f() 
...     yield 42
>>> k = g()
>>> next(k)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in g
  File "<stdin>", line 2, in f
ZeroDivisionError: division by zero
>>> next(k)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

可以看到,函数 f 将会产生 ZeroDivisionError 异常,而生成器 g 没有捕获该异常,所以该异常会继续向调用者传递,于是我们在解释器中看到ZeroDivisionError 异常。同时,因为生成器 g 中产生了异常,这也导致了生成器停止。那么,再下一次使用 next 调用 g 时,就会得到 StopIteration 异常。

总结: yield 语句能将一个普通函数变成生成器。调用该生成器将会一个生成器对象,这时生成器内部并不会执行代码。要使得生成器真正执行,我们可以使用 next 方法,当然也可以使用 for 循环来迭代。当生成器遇到了 return 语句,引发了异常,或者执行到了函数尾,都会停止。那么下一次再使用该生成器时,就会得到 StopIteration 异常。

参考: PEP 234 – Iterators PEP 255 Simple Generator

posted @ 2017-07-26 14:52  天涯海角路  阅读(163)  评论(0)    收藏  举报