生成器是一种特殊的迭代器,内部支持了生成器协议,不需要明确定义__iter__()和next()方法。生成器通过生成器函数产生,生成器函数可以通过常规的def语句来定义,但是不用return返回,而是用yield一次返回一个结果。
一、yield和迭代器生成器
迭代器是非常高效的类型,无论是从时间复杂度,还是从空间复杂度。而实现迭代器的代码虽然简单,却也繁琐。为此,python定义了一个yield关键字,专门用来构造迭代器。yield有生成,产生的意思。
yield的功能和return非常类似,它们都只能在方法中使用。不同的是,包含yield语句的方法被称为生成器方法。当调用生成器方法时,会返回一个生成器对象。
例如,看下面的例子。
def MyGenerator(): yield 1 gen = MyGenerator() print gen
输出结果为
<generator object MyGenerator at 0x0000000001D9DD80>
当调用生成器对象的next方法时,会执行生成器方法中的代码,直至遇到yield语句时,方法的执行过程会被挂起。同时,方法运行的上下文环境会被保存。而next方法的返回值就是yield关键字后面表达式的返回值。
例如,下面代码
print gen.next()
执行结果为
1
当我们继续调用next方法时,从上一次挂起的地方开始,继续执行后面的代码。直至遇到下一个yield语句。当方法执行完毕,依然没有遇到yield语句,抛出StopIteration异常。
例如
def MyGenerator(): yield 1 yield 'a' gen = MyGenerator() print gen.next() print gen.next() print gen.next()
上面代码中第1次调用next方法,执行语句yield 1。第2次调用next方法,执行语句yield 'a'。第3次调用next方法时,在方法退出前都没有遇到yield语句,因此抛出StopIteration异常。
上面介绍的生成器方法的工作机理。在后面的博文中,会逐步介绍生成器方法的一些经典应用。
二、通过生成器函数构造序列对象的迭代器
事实上,一个序列对象的迭代器,依赖于一个整数序列的迭代器。看下面的代码
def MyGenerator(len): start = 0 while start < len: yield start start = start + 1 gen = MyGenerator(3) print gen.next() print gen.next() print gen.next() print gen.next()
当调用第1次next方法时, 会首先执行MyGenerator方法的第1行代码start = 0。然后进入循环。这里len的值通过参数传入为3。因此while的条件表达式为真。进入循环后,遇到yield语句,方法的执行过程被挂起。next方法的返回值为start的值,即0。
当调用第2次next方法时,接着上面的挂起点,往下执行start = start + 1语句,start的值变为1。接着又进入while循环的条件判断,start<len依然为真。因此,又执行yield语句。但是由于start值为1,故而这一次next方法返回的值为1。
第3次next方法的调用类似。
当调用第4次next方法时,while循环的条件判断start < len为假,while循环结束,MyGenerator方法调用也随之结束,抛出StopIteration异常。
输出结果
0 1 2 Traceback (most recent call last): File "test.py", line 21, in <module> print gen.next() StopIteration
有了上面的结果,重写序列对象的迭代器轻而易举。
def MyGenerator(sequence): start = 0 while start < len(sequence): yield sequence[start] start = start + 1 gen = MyGenerator([1,2,3,'a','b','c']) for i in gen: print i
对比之前迭代器类的代码,我们可以认识到,yield关键字为构造迭代器提供了多大的方便。它使得代码长度缩减许多,同时也大大增强了可读性。
三、生成器对象的send方法
生成器对象是一个迭代器。但是它比迭代器对象多了一些方法,它们包括send方法,throw方法和close方法。这些方法,主要是用于外部与生成器对象的交互。本文先介绍send方法。
send方法有一个参数,该参数指定的是上一次被挂起的yield语句的返回值。这样说起来比较抽象,看下面的例子。
def MyGenerator(): value = (yield 1) value = (yield value) gen = MyGenerator() print gen.next() print gen.send(2) print gen.send(3)
输出的结果如下
1 2 Traceback (most recent call last): File "test.py", line 18, in <module> print gen.send(3) StopIteration
上面代码的运行过程如下。
当调用gen.next()方法时,Python首先会执行MyGenerator方法的yield 1语句。由于是一个yield语句,因此方法的执行过程被挂起,而next方法返回值为yield关键字后面表达式的值,即为1。
当调用gen.send(2)方法时,python首先恢复MyGenerator方法的运行环境。同时,将表达式(yield 1)的返回值定义为send方法参数的值,即为2。这样,接下来value=(yield 1)这一赋值语句会将value的值置为2。继续运行会遇到yield value语句。因此,MyGenerator方法再次被挂起。同时,send方法的返回值为yield关键字后面表达式的值,也即value的值,为2。
当调用send(3)方法时MyGenerator方法的运行环境。同时,将表达式(yield value)的返回值定义为send方法参数的值,即为3。这样,接下来value=(yield value)这一赋值语句会将value的值置为3。继续运行,MyGenerator方法执行完毕,故而抛出StopIteration异常。
总的来说,send方法和next方法唯一的区别是在执行send方法会首先把上一次挂起的yield语句的返回值通过参数设定,从而实现与生成器方法的交互。但是需要注意,在一个生成器对象没有执行next方法之前,由于没有yield语句被挂起,所以执行send方法会报错。例如
gen = MyGenerator() print gen.send(2)
上面代码的输出为
Traceback (most recent call last): File "test.py", line 16, in <module> print gen.send(2) TypeError: can't send non-None value to a just-started generator
当然,下面的代码是可以接受的
gen = MyGenerator() print gen.send(None)
因为当send方法的参数为None时,它与next方法完全等价。但是注意,虽然上面的代码可以接受,但是不规范。所以,在调用send方法之前,还是先调用一次next方法为好。
四、生成器对象的throw方法
上边介绍的send方法,通过向生成器对象传递参数来实现与生成器对象的交互。本文介绍与生成器对象的另一种方式,即throw方法。它的实现手段是通过向生成器对象在上次被挂起处,抛出一个异常。之后会继续执行生成器对象中后面的语句,直至遇到下一个yield语句返回。如果在生成器对象方法执行完毕后,依然没有遇到yield语句,抛出StopIteration异常。
请看下面的例子
def myGenerator(): value = 1 while True: yield value value += 1 gen = myGenerator() print gen.next() print gen.next() print gen.throw(Exception, "Method throw called!")
输出结果为
1 2 Traceback (most recent call last): File "test.txt", line 11, in <module> print gen.throw(Exception, "Method throw called!") File "test.txt", line 4, in myGenerator yield value Exception: Method throw called!
代码的最后一句向生成器对象抛出了一个异常。但是,在生成器对象的方法时没有处理该异常的代码,因此异常会被抛出到主方法。
下面的示例中,添加了处理异常的代码
def myGenerator(): value = 1 while True: try: yield value value += 1 except: value = 1 gen = myGenerator() print gen.next() print gen.next() print gen.throw(Exception, "Method throw called!")
在上面的代码中,加入了一个try-except语句块处理异常。当生成器方法收到异常后,会调到except语句块,将value置为1。因此,代码的输出如下。
1 2 1 Exception RuntimeError: 'generator ignored GeneratorExit' in <generator object myGenerator at 0x00000000028BB900> ignored
上面输出中,第2个1是gen.throw方法的返回值。在执行完该方法后,生成器对象方法的while循环并没有结束,也即是说生成器方法的执行还没有结束。这个时候如果强制结束主程序,会抛出一个RuntimeError。也就是上面输出的第4行。要优雅地关闭主程序,需要用到生成器对象的close方法。
五、GeneratorExit异常
当一个生成器对象被销毁时,会抛出一个GeneratorExit异常。请看下面的代码。
def myGenerator(): try: yield 1 except GeneratorExit: print "myGenerator exited" gen = myGenerator() print gen.next()
输出结果为
1
myGenerator exited
上面代码的运行逻辑如下: 当调用到gen.next()方法时,会执行生成器对象方法的yield语句。此后,主程序结束,系统会自动产生一个GeneratorExit异常,被生成器对象方法的Except语句块截获。
而GeneratorExit异常产生的时机,是在生成器对象被销毁之前。为了验证这点,请看下面的代码。
def myGenerator(): try: yield 1 yield 2 except GeneratorExit: print "myGenerator exited" gen = myGenerator() print gen.next() del gen print "Main caller exited"
输出结果
1
myGenerator exited
Main caller exited
值得一提的是,GeneratorExit异常只有在生成器对象被激活后,才有可能产生。更确切的说,需要至少调用一次生成器对象的next方法后,系统才会产生GeneratorExit异常。请看下面的代码。
def myGenerator(): try: yield 1 yield 2 except GeneratorExit: print "myGenerator exited" gen = myGenerator() del gen print "Main caller exited"
其输出结果如下:
Main caller exited
在上面的示例中,我们都显式地捕获了GeneratorExit异常。如果该异常没有被显式捕获,生成器对象也不会把该异常向主程序抛出。因为GeneratorExit异常定义的初衷,是方便开发者在生成器对象调用结束后定义一些收尾的工作,如释放资源等。
六、生成器对象的close方法
生成器对象的close方法会在生成器对象方法的挂起处抛出一个GeneratorExit异常。GeneratorExit异常产生后,系统会继续把生成器对象方法后续的代码执行完毕。参见下面的代码。
def myGenerator(): try: yield 1 print "Statement after yield" except GeneratorExit: print "Generator error caught" print "End of myGenerator" gen = myGenerator() print gen.next() gen.close() print "End of main caller"
代码执行过程如下:
- 当调用gen.next方法时,会激活生成器,直至遇到生成器方法的yield语句,返回值1。同时,生成器方法的执行被挂起。
- 当调用gen,close方法时,恢复生成器方法的执行过程。系统在yield语句处抛出GeneratorExit异常,执行过程跳到except语句块。当except语句块处理完毕后,系统会继续往下执行,直至生成器方法执行结束。
代码的输出如下:
1
Generator error caught
End of myGenerator
End of main caller
需要注意的是,GeneratorExit异常的产生意味着生成器对象的生命周期已经结束。因此,一旦产生了GeneratorExit异常,生成器方法后续执行的语句中,不能再有yield语句,否则会产生RuntimeError。请看下面的例子。
def myGenerator(): try: yield 1 print "Statement after yield" except GeneratorExit: print "Generator error caught" yield 3 gen = myGenerator() print gen.next() gen.close() print "End of main caller"
输出结果为
1 Generator error caught Traceback (most recent call last): File "test.txt", line 12, in <module> gen.close() RuntimeError: generator ignored GeneratorExit
注意,由于RuntimError会向主方法抛出,因此主方法最后的print语句没有执行。
有了上面的知识,我们就可以理解为什么下面的代码会抛出RuntimError错误了。
def myGenerator(): value = 1 while True: try: yield value value += 1 except: value = 1 gen = myGenerator() print gen.next() print gen.next() print gen.throw(Exception, "Method throw called!")
上面代码中,当主程序结束前,系统产生GeneratorExit异常,被生成器对象方法的except语句捕获,但是此时while语句还没有退出,因此后面还会执行“yield value”这一语句,从而发生RuntimeError。要避免这个错误非常简单,请看下面的代码。
def myGenerator(): value = 1 while True: try: yield value value += 1 except Exception: value = 1 gen = myGenerator() print gen.next() print gen.next() print gen.throw(Exception, "Method throw called!")
代码第7行的except语句声明只捕获Exception异常对象。这样,当系统产生GeneratorExit异常后,不再被except语句捕获,继续向外抛出,从而跳出了生成器对象方法的while语句。
这里再简单说一句,GeneratorExit异常继承自BaseException类。BaseException类与Exception类不同。一般情况下,BaseException类是所有内建异常类的基类,而Exception类是所有用户定义的异常类的基类。
转自:http://blog.csdn.net/hedan2013/article/details/72811117