zoukankan      html  css  js  c++  java
  • python协程系列(一)——生成器generator以及yield表达式详解

      参考:https://blog.csdn.net/qq_27825451/article/details/85226239

      声明:本文将详细讲解python协程的实现机理,为了彻底的弄明白它到底是怎么一回事,鉴于篇幅较长,将彻底从最简单的yield说起从最简单的生成器开始说起,因为很多看到这样一句话的时候很懵,即“yield也是一种简单的协程”,这到底是为什么呢?本次系列文章“python协程系列文章”将从最简单的生成器、yield、yield from说起,然后详细讲解asyncio的实现方式。本文主要讲解Python的生成器的各种详细操作,以及yield表达式的详细应用。

      一,生成器generator详解

      注意:关于什么是可迭代对象、什么是生成器、什么是迭代器这里不再赘述。

      可以参考:https://www.cnblogs.com/minseo/p/15357586.html

      yield是实现生成器的重要关键字,但是yield语句有一些非常重要的小细节需要注意,可能我们在写一个简单的生成器的时候有很多东西没有用到,这里将分情况逐一介绍。特别是生成器的三个重要方法,一个是next(),一个是send(),一个是throw(),他们到底有什么样的作用。

      1,最简单的生成器

      test.py

    # 最简单的生成器
    # 该生成器函数传递一个整数,然后遍历,生成器输出为0,1,2,3...n-1
    def my_generator(n):
        for i in range(n):
            yield i
    
    for i in my_generator(5):
        print(i,end=' ')
    # 0 1 2 3 4    
    

       2,send()方法使用

    # send()方法的使用
    def my_generator(n):
        for i in range(n):
            temp = yield i
            print('我是%s,是通过send方法传递的参数' %(temp))
    
    g = my_generator(5)
    # 初始化生成器,相当于send(None)
    # 如果使用send(None)初始化,第一次传递的参数必须为None
    # 第一次迭代输出0
    print(next(g))
    # 0
    # 第二次迭代返回值为1 运行yield后代码输出temp的值,因为没有传递使用为None
    print(next(g))
    # 我是None,是通过send方法传递的参数
    # 1
    # 传递参数100,执行打印输出temp的值
    # send也是有返回值的,为yield后的值,这里没有使用print输出
    g.send(100)
    # 我是100,是通过send方法传递的参数
    # 第四次迭代返回值为3 运行yield后代码输出temp的值,因为没有传递使用为None
    print(next(g))
    # 3
    # 我是None,是通过send方法传递的参数
    # 第五次迭代,执行到yield停止了,循环结束了,没有执行到print这步
    print(next(g))
    # 4
    

     

      对应的语句输出如下

       从上面可以看出yield语句与普通函数的return语句的区别在哪里了,主要集中在以下几点

      (1)return不能写成“temp=return xxx”的形式,会提示语法错误,但是yield可以写成"temp=yield xxx"的形式

      (2)普通函数return后面的语句都是不会再执行的,但是yield语句后面的依然会执行,但是需要注意的是,由于“延迟加载”特性,yield后面的代码并不是在第一次迭代的时候执行的,而是在第二次迭代的时候才执行第一次yield后面没有执行的代码。也正是这个特性,构成了yield为什么是实现协程最简单的实现。

      个人备注:如果在生成器函数中定义return则遇到return则会报StopIteration错误立即退出生成器

      (3)使用send()方法传进去的值,实际上就是yield表达式返回的值,这就是为什么前面每次输出print(temp)都打印出None,因为没有send值,所以temp为None,但是send(100)之后却打印100,因为此时temp就是100了。

      个人备注:使用send()方法传递进去的值是赋值给yield前面的变量temp了,yield的返回值是yield后的值,这个返回值也是send()方法的返回值,即send()方法接收的返回值为yield的返回值

      我甚至还可以在yield后面不放任何东西,如下代码:

    # send()方法的使用
    # yield不设置返回值
    def my_generator(n):
        for i in range(n):
            temp = yield 
            print('我是%s,是通过send方法传递的参数' %(temp))
            
    g = my_generator(5)
    print(next(g))
    print(next(g))
    g.send(100)
    print(next(g))
    print(next(g))
    # None
    # 我是None,是通过send方法传递的参数
    # None
    # 我是100,是通过send方法传递的参数
    # 我是None,是通过send方法传递的参数
    # None
    # 我是None,是通过send方法传递的参数
    # None
    

      3,yield语句的用法总结

      yield的一般形式为:

      temp=yield 表达式(每次迭代要返回的值)

      (1)如果要返回确定的值,后面的表达式不可省略,绝大部分情况下我们也不省略,否则只能返回None

      (2)如果使用了send(value),传递进去的那个value会取代那个表达式的值,并且会将传递进去的那个值返回给yield表达式的结果temp,所以如果想要在yield后面使用传递进去的那个值,必须要有使用temp,否则无法使用

      (3)yield语句的一般形式

    temp = yield expression(推荐:既可以返回迭代的值,也可以接受send进去的参数并使用)
    yield expression(也可以使用:只不过不能接受send传递的值)
    temp = yield(不推荐 yield没有设置返回值)
    yield(不推荐)
    

      4,迭代器(生成器)的send()方法详解

      主要目的是交互

      查看send的定义,得到send(arg)是有返回值的,而且他的返回值就是原本我一个迭代处理的那个值,如下所示

    # send的返回值
    def my_generator(n):
        for i in range(n):
            yield i
     
    g=my_generator(5)
     
    print(next(g))
    print(next(g))
    g.send(100)
    print(next(g))
    print(next(g))
    # 0
    # 1
    # 3
    # 4
    

      我们发现虽然100传进去了,但是他并没有迭代出来,那原来的2去哪里了呢?send(100)实际上就是返回的2

      如果改为以下代码获取send的返回值

    # send返回值2
    def my_generator(n):
        for i in range(n):
            yield i
     
    g=my_generator(5)
     
    print(next(g))
    print(next(g))
    a = g.send(100)
    print('我是send的返回值%s' %(a))
    print(next(g))
    print(next(g))
    # 0
    # 1
    # 我是send的返回值2
    # 3
    # 4
    

      send(arg)方法总结

      (1)它的主要作用是,单位需要手动更改生成器成某一个值并且使用它,则send发送进去一个暑假,然后报错到yield语句的返回值,以提供使用。

      (2)send(arg)的返回值就是那个本来应该被迭代出来的那个值。这样既可以保证我能传入新的值,原来的值也不会弄丢。

      5,生成器的throw方法用法

      这个函数相比较于前面的next()、send()来说更加复杂,先看一下它的函数描述:

      raise exception in generator,return next yielded value or StopIteration,即在生成器中抛出异常,并且这个throw函数会返回下一个要迭代的值或者是StopIteration。还是通过几个例子来看吧!

    # throw()
    def my_generator():
        yield 'a'
        yield 'b'
        yield 'c'
    g=my_generator()
    print(next(g))
    # a
    print(next(g))
    # b
    print('-------------------------')
    # 往生成器抛出异常
    print(g.throw(StopIteration))
    # StopIteration
    # print(g.throw(TypeError))
    # 生成器异常以后推出,以下语句不会执行
    print(next(g))
    

      输出如下

       因为在迭代完 b 之后,就触发了StopIteration异常,这相当于后面的 ‘c’ 已经没用了,跳过了c ,c再也不会执行,就中断了,所以后面的 'c'再也不会迭代,所以这里不会再返回任何值,返回的是StopIteration。

      再看一个例子

    # throw()另外一个例子
    def my_generator():
        try:
            yield 'a'
            yield 'b'
            yield 'c'
            yield 'd'
            yield 'e'
        except ValueError:
            print('触发“ValueError"了')
        except TypeError:
            print('触发“TypeError"了')
     
    g=my_generator()
    print(next(g))
    print(next(g))
    print('-------------------------')
    print(g.throw(ValueError))
    # 触发异常,以下语句不执行
    print('-------------------------')
    print(next(g))
    print(next(g))
    print('-------------------------')
    print(g.throw(TypeError))
    print('-------------------------')
    print(next(g))
    

      输出如下

       本例和上例差不多,都是往生成器抛入一个错误导致生成器产生StopIteration错误,本例使用try...except...捕获ValueError错误,接着生成器抛出StopIteration错误,整个生成器结束了,往后代码不继续执行了

      当前面两次执行了a和b之后,向生成器扔进去一个异常,触发ValueError异常,这时候意味着try后面的c、d、e已经作废了,不会再有用,这个生成器已经终止了,因此g.throw()会返回StopIteration。

      再看一个例子:

    # throw()再看一个例子
    def my_generator():
        while True:
            try:
                yield 'a'
                yield 'b'
                yield 'c'
                yield 'd'
                yield 'e'
            except ValueError:
                print('触发“ValueError"了')
            except TypeError:
                print('触发“TypeError"了')
     
    g=my_generator()
    print(next(g))
    # a
    print(next(g))
    # b
    print('-------------------------')
    # -------------------------
    # 往生成器抛入ValueError错误首先执行捕获错误打印 触发“ValueError"了
    # 因为使用while True无限循环,此时触发错误后重新执行循环返回下一个值,而不返回StopIteration错误了
    # 重新执行循环的下一个值是a所以打印返回值a
    print(g.throw(ValueError))
    # 触发“ValueError"了
    # a
    print('-------------------------')
    # -------------------------
    # 因为上一步以及执行到a了所以下面next方法继续打印以后的yield值即b c
    print(next(g))
    # b
    print(next(g))
    # c
    print('-------------------------')
    # -------------------------
    # 往生成器抛入TypeError错误首先执行捕获错误打印 触发“TypeError"了
    # 因为使用while True无限循环,此时触发错误后重新执行循环返回下一个值,而不返回StopIteration错误了
    # 重新执行循环的下一个值是a所以打印返回值a
    print(g.throw(TypeError))
    # 触发“TypeError"了
    # a
    print('-------------------------')
    # -------------------------
    # 因为上一步以及执行到a了所以下面next方法继续打印以后的yield值即b
    print(next(g))
    # b
    

      解释:

      出现这样的结果是不是很意外?它和上面的那个例子只有一个while之差,我们结果差那么多,解释如下:

      首先print(next(g))两次:会输出a,b,并停留在c之前.

      然后由于执行了g.throw(ValueError),所以会跳过所有后续的try语句,也就是说yield 'c'、yield 'd'、yield 'e'不会被执行,然后进入到except语句,打印出 触发“ValueError"了。然后再次进入到while语句部分,消耗一个yield,此时因为是重新进入的while,消耗的依然是第一个yield 'a',所以会输出a。实际上这里的a也就是g.throw()的返回值,因为它返回的是下一个迭代的值;

      然后在print(next(g))两次,会执行yield b’、yield 'c’语句,打印出b、c,并停留在执行完该语句后的位置,即yield 'd'之前。

      然后再g.throw(TypeError):会跳出try语句,从而后面的d,e不会被执行,下次自一次进入while,依然打印出a。

      最后,执行了一次print(next(g)),打印出b。

      输出如下

     

       说明:为什么加while可以使方法throw返回下一个yield的值,而不是抛出StopIteration错误,因为while True无限循环,当使用throw往生成器抛入错误时,因为while内部使用是try捕获错误,所以即使发生错误也不会直接抛出错误而是根据错误类型执行对应的except,执行完except以后继续执行while无限循环继续执行生成器。像上一个例子没有使用while是可以捕获到对应的错误,因为没有while语句所以没有无限循环则在抛入错误以后继续往下执行但是因为继续往下执行没有yield了所以抛出StopIteration错误。

      下面使用调试模式分析执行过程

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

       注意:本例手动往生成器抛入几个错误观察throw()方法的返回,throw()方法返回下一个值或者StopIteration。

          如果不手动往生成器抛入错误而是一直执行next()方法则本生成器是一个无限的生成器,生成器的内容为a b c d e重复无限输出

      6,生成器的启动与关闭close

      (1)生成器的启动

      使用close()方法手动关闭生成器函数,后面的调用会直接返回StopIteration异常

      这里所讨论的启动不是使用for循环迭代,我们在使用for循环迭代的时候可能没有去考虑“启动”与“关闭”这些事情,这里指的是使用next()内置方法一个一个迭代的情形。在第一次迭代的时候,一定要先启动生成器,启动的两种方法为:

      第一:直接使用next(g),这会直接开始迭代第一个元素(推荐使用这个启动)

      第二:使用g.send(None)进行启动,注意第一次启动只能传入None。如果传入其他具体的值则会报错。

    def my_generator():
        yield 1
        yield 2
        yield 3
        yield 4
     
    g = my_generator()
    # 第一次启动,本来第一次应该迭代的1,这里被取代了,但是send(None)会返回1
    # g.send(None)等价于next(g)
    g.send(None)  
    print(next(g))
    # 2
    print(next(g))
    # 3
    print(next(g))
    # 4
    

      (2)生成器关闭通过close()方法

      如果一个生成器被中途关闭之后,在此调用next()方法,则会显示错误,如下:

    # 关闭生成器close()方法
    
    def my_generator():
        yield 1
        yield 2
        yield 3
        yield 4
     
    g = my_generator()
    print(next(g))
    # 1
    print(next(g))
    # 2
    g.close()
    # #在此处会显示错误
    print(next(g))   
    

      7,生成器终止迭代-StopIteration

      前面讲的手动关闭生成器,使用close()方法,后面的迭代或抛出StopIteration异常。另外

      在一个生成器中,如果没有return,则默认执行到函数完毕时返回StopIteration;

    def g1():
        yield 1
    
    g = g1()
    # 第一次调用next(g)时,会在执行完yield语句后挂起,使用此时程序并没有执行结束
    next(g)
    # 第二次执行next(g)时,程序试图从yield的下一条语句开始执行,发现已经到了结尾,所以抛出StopIteration异常
    next(g)
    

      如果遇到return,如果在执行过程中return,则直接抛出StopIteration终止迭代

    # 遇到return立即抛出StopIteration异常
    def g2():
        yield 'a'
        return
        yield 'b'
    
    g = g2()
    # 执行完以下语句程序停留在yield 'a'语句后的位置
    next(g)
    # 程序发现下一条语句是return所以抛出StopIteration异常,这样yield 'b'语句永远也不会执行
    next(g)
    # StopIteration
    

      如果在return后返回一个值,那么这个值为StopIteration异常的说明,不是程序的返回值

    # return如果有返回值则是异常StopIteration的说明而不是函数的返回值
    def g3():
        yield 'a'
        return '这是错误说明'
        yield 'b'
    
    g = g3()
    next(g)
    next(g)
    

      运行结果为

       注意:生成没有办法使用return来返回值。因为return返回的那个值是通过StopIteration的异常信息返回的,所以没有办法直接获取这个return返回的值。

      当然上面说的无法获取return返回值,我们指的是没有办法通过result=g3()这种形式获取return的返回值。实际上还是有手段获取这个return的值,有两种方法

      方法一,使用后面的yield from语句(下文再讲解)

      方法二,因为return返回的值是作为StopIteration的一个value属性存在的,StopIteration本质是是一个类,所以可以通过访问它的value属性获取这个return返回的值,使用以下代码

    # 获取return的返回值
    def g3():
        yield 'a'
        return '这是错误说明'
        yield 'b'
    
    g = g3()
    try:
        # 迭代a
        next(g)
        # 触发异常
        next(g)
    except StopIteration as exc:
        result = exc.value
        print(result)
    
    # 这是错误说明
    

      

      


      

      

      

  • 相关阅读:
    Spring Boot 自定义starter
    jvm中的年轻代 老年代 持久代 gc
    nginx反向代理服务器端口问题
    ACE Editor在线代码编辑器简介及使用引导
    Linux下MySQL 5.6.24的编译安装与部署
    C3p0的参数
    Mysql 查看连接数,状态
    linux下mysql定时备份数据库
    Mysql中存储方式的区别
    mysql常用语句
  • 原文地址:https://www.cnblogs.com/minseo/p/15391722.html
Copyright © 2011-2022 走看看