理解 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 异常。

浙公网安备 33010602011771号