• python生成器(转)


    生成器是一种特殊的迭代器,内部支持了生成器协议,不需要明确定义__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

  • 相关阅读:
    [Next] 六.next的优化
    [Next] 五.next自定义内容
    Mac解决端口占用
    [Next] 四.在next中引入redux
    [Next] 服务端渲染知识补充
    [Next] 三.next自定义服务器和路由
    哪些使用UDP、TCP、IP协议
    IDEA配置git
    ssm整合配置
    git传输远程仓库
  • 原文地址:https://www.cnblogs.com/shixisheng/p/7090733.html
Copyright © 2020-2023  润新知