zoukankan      html  css  js  c++  java
  • Python学习之旅—生成器对象的send方法详解

    前言

        在上一篇博客中,笔者带大家一起探讨了生成器与迭代器的本质原理和使用,本次博客将重点聚焦于生成器对象的send方法。


    一.send方法详解

        我们知道生成器对象本质上是一个迭代器。但是它比迭代器对象多了一些方法,它们包括send方法,throw方法和close方法等。生成器拥有的这些方法,主要用于外部与生成器对象的交互。我们来看看生成器对象到底比迭代器多了哪些方法:

    def func():
        yield 1
    g = func()
    item_list = [1, 2, 3, "spark", "python"]
    list_iterator = item_list.__iter__()
    print(set(dir(g))-set(dir(list_iterator)))
    #打印结果:{'__del__', 'send', '__name__', '__qualname__', 'gi_yieldfrom', 'gi_frame', 'throw', 'gi_running', 'gi_code', 'close'}

          send方法有一个参数,该参数指定的是上一次被挂起的yield语句的返回值。正确的语法是:send(value)我们还是通过实际的代码进行讲解说明:

    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 "D:/pythoncodes/day13.py", line 180, in <module>
        print(gen.send(3))
    StopIteration

    根据执行结果我们一起来分析下上面代码的运行过程:

    【001】当调用gen.__next__()方法时,Python首先会执行生成器函数MyGenerator的yield 1语句。由于是一个yield语句,因此方法的执行过程被挂起,而__next__()方法的返回值为yield关键字后面表达式的值,即为1,所以首先会打印出1。


    【002】当调用gen.send(2)方法时,Python首先恢复MyGenerator方法的运行环境,上一次程序执行到yield1时被挂起,此时恢复了运行环境,继续开始执行。开始执行的第一步是将将表达式(yield 1)的返回值定义为send方法参数的值,即为2。这样,接下来value=(yield 1)这一赋值语句会将value的值置为2。继续运行会遇到yield value语句,因此,生成器函数MyGenerator会再次被挂起。同时,send方法的返回值为yield关键字后面表达式的值,也即value的值,为2。由于又遇到了yield语句,所以此时生成器函数又会被挂起,直到等待下一次的__next__()方法调用。


    【003】当调用send(3)方法时,Python又恢复了MyGenerator方法的运行环境。同样,开始执行的第一步是将表达式(yield value)的返回值定义为send方法参数的值,即为3。这样,接下来value=(yield value)这一赋值语句会将value的值置为3。继续运行,MyGenerator方法执行完毕,故而抛出StopIteration异常。因为在语句value=(yield value)执行完毕后,后面就没有yield value语句了,即使我们执行gen.send(3),然后打印出来也拿不到3这个值,因为其压根没有被返回。

    总的来说,send方法和next方法唯一的区别在于:执行send方法会首先把上一次挂起的yield语句的返回值通过参数设定,从而实现与生成器方法的交互。但是需要注意,在一个生成器对象没有执行next方法之前,由于没有yield语句被挂起,所以执行send方法会报错。例如

    def MyGenerator():
        value = yield 1
        value = yield value
    
    gen = MyGenerator()
    print(gen.send(2))
    #报错:TypeError: can't send non-None value to a just-started generator

    可以看到在没有先执行.__next__方法时,直接执行send()方法会报错,因此我们可以将.__next__方法看作是生成器函数函数体执行的驱动器。因此我们可以做出如下的总结:

      使用 send() 方法只有在生成器挂起之后才有意义,也就是说只有先调用__next__方法激活生成器函数的执行,才能使用send()方法。

    如果真想对刚刚启动的生成器使用 send 方法,则可以将 None 作为参数进行调用。也就是说, 第一次调用时,要使用 g.__next__方法或 send(None),因为没有 yield 语句来接收这个值。 虽然我们可以使用send(None)方法,但是这样写不规范,所以建议还是使用__next__()方法。

      清楚了上面这点,我们就能很好地理解gen.send()方法的本质了。可能还有小伙伴对生成器函数中的yield赋值语句的执行流程不是很了解,下面我就来通过大白话的形式来为各位朋友讲解。

      我们知道从程序执行流程来看,赋值操作的 = 语句都是从右边开始执行的。既然能够明确这一点,那我们就能很好理解yield赋值语句了.

      依然是上面的程序,来看 x = yield i 这个表达式,如果这个表达式只是x = i, 相信每个人都能理解是把i的值赋值给了x,而现在等号右边是一个yield i,我们称之为yield表达式,既然是表达式,肯定要先要执行yield i,然后才是赋值。yield把i值返回给调用者后,执行的下一步操作是赋值,本来可以好好地赋值给x,但却因为等号右边的yield关键字而被暂停,此时生成器函数执行到赋值这一步,换句话说x = yield i这句话才执行了一半。

      当调用者通过调用send(value)再次回到生成器函数时,此时是回到了之前x = yield i这个赋值表达式被暂停的那里,刚才我们说过生成器函数执行到了赋值这一步,因此接下来就要真正开始执行赋值操作啦,也即是执行语句x = yield i的另一半过程:赋值。这个值就是调用者通过send(value)发送进生成器的值,也即是yield i这个表达式的值。


    二.题目解析

      在弄清楚send()方法的本之后,我们来练习几个题目加深对该知识点的理解。 

    【001】

    def func():
        print(123)
        value = yield 1
        print(456)
        yield '***'+value+'***'
    
    g = func()
    print(g.__next__())
    print(g.send('aaa'))
    # 打印结果为:123 1 456 ***aaa***

    【002】

    def func():
        print(123)
        value = yield 1
        print(value)
        value2 = yield '*****'
        print(value2)
        yield
    
    g = func()
    print(g.__next__())
    print(g.send('aaa'))
    print(g.send('bbb'))
    # 打印值为:123 1 aaa ***** bbb None
    本体需要仔细分析,注意一点,不管是g.__next__()方法还是g.send(value)的返回值都是yield后面的值。所以最后才会打印出None,千万不要被里面的
    print语句搞混了

    【003】

    def func():
        print('*')
        value = yield 1
        print('**', value)
        yield 2
        yield 3
    
    g = func()
    print(g.__next__())
    print(g.send('aaa'))
    print(g.__next__())
    # 打印结果:* 1  ** aaa 2 3 其中**和aaa是一起的。

    【004】

    def func():
        print('*')
        value = yield 1
        print('**', value)
        v = yield 2
        print(v)
        yield 3
    
    g = func()
    print(g.__next__())
    print(g.send('aaa'))
    print(g.send('bbb'))
    # 打印结果:* 1  ** aaa  2 bbb 3 其中**和aaa是一起的。

    【005】我们再来看如下一个经典的例子:

    def func():
        print(1)
        yield 2
        print(3)
        value = yield 4
        print(5)
        yield value
    
    g = func()
    print(g.__next__())
    print(g.send(88))
    print(g.__next__())
    #打印结果如下:1 2 3 4 5 None

    本题是一道比较经典的send方法面试题,我们一起来分析下该方法的执行流程:当执行完g.__next__()方法后,该方法的返回值为2,因为yield后面是2。此时我们接着执行g.send(88),这里非常容易搞迷糊,我们发现再次进入到生成器函数体中时,按照我们前面所说的执行步骤,此时应该将88赋值给(yield 2)这个表达式,但是我们意外地发现左边并没有变量来接收这个88。此时我们继续往下面执行,接着打印3,然后遇到value = yield 4,因此这里暂停执行,将4返回给g.send(88),因此紧接着打印4,然后接着执行最后一行g.__next__(),此时再次进入生成器函数体中上次的执行位置value = yield 4,由于此时执行的是g.__next__()方法,并没有传入任何值,相当于调用方法g.send(None),所以此时表达式(yield 4)的值为None,并将None赋值给value;接着执行下面的print(5),然后继续执行下面的yield value语句,由于value的值为None,此时又碰到了yield语句,所以最后一行g.__next__()方法打印的值为None.所以最终的打印结果是1 2 3 4 5 None。这是一道比较经典的题目,希望大家能够仔细分析下题目,认真分析下相关执行流程。

    【006】生成器与生成器表达式,列表的结合使用

    def demo():
        for i in range(4):
            yield i
    g = demo()
    
    g1 = (i for i in g)print(g1)
    g2 = (i for i in g1)
    print(g2)
    print(list(g1))
    print(list(g2))
    #打印结果如下:

    <generator object <genexpr> at 0x000001D59303BE60>
    <generator object <genexpr> at 0x000001D59305F678>
    [0, 1, 2, 3]
    []

    笔者开始拿到这个题目的时候也比较懵逼,我们首先来一步步分析:g1是一个生成器对象,它是由生成器表达式(i for i in g)生成的。g本身是一个生成器对象,那么for

    i in g表示我们使用for循环来迭代遍历生成器对象,但是本题写成了生成器表达式的形式(i for i in g),因此这里返回的又是一个生成器对象,按照刚才的分析,g2也是一个生成器对象,此时即使我们打印g1和g2,打印出的也只是这两个对象在内存中地址值,为什么没有打印出实际的值呢?因为此时我们压根就没有用到g1和g2里面的值,所以它们也就不会执行。

            直到我们使用list(g1)时,才触发了真正的计算,首先是for i in g,前面一篇博客我们讲解了for循环的本质,它其实是不断调用迭代器的__next__()方法来获取迭代器里面的值,因此这里相当于不断遍历生成器对象g,然后获取g里面的值,由于生成器函数的函数体for循环只会执行4次,所以当执行完毕for i in g后,取出了0,1,2,3四个值,然后再使用list(g)将这四个值封装在列表中。因此我们打印print(list(g1)),其实打印的是生成器对象里面的元素。紧接着,我们来执行第二个print(list(g2))语句,同理在这里会执行for i in g1,这句话的意思是通过for循环来迭代遍历生成器对象g1里面的元素,不过这里大家要注意的是此时我们已经将g1中的元素遍历完毕了,而且封装在列表中。因此再次遍历g1时,已经没有元素了,所以即使使用list(g2)再来封装,也只是一个空列表而已,所以最终的结果打印的就是一个空列表。

      我们换一种角度来思考这个问题,如果我们先取g2里面的元素,然后再取g1里面的元素,会打印出什么?还是来看代码:

    def demo():
        for i in range(4):
            yield i
    g = demo()
    
    g1 = (i for i in g)
    g2 = (i for i in g1)
    
    print(list(g2))
    print(list(g1))

    同样我们再来分析下,由于g2是从g1中取值,所以当使用list将g2中的元素封装完毕后,g1中的元素也被访问完毕了!此时再打印g1,然后使用list封装也于事无补,依然是一个空列表而已。所以最终打印结果为:0,1,2,3 [ ]

    【007】生成器与装饰器的结合使用

    这里举一个实际的例子,我们首先来看看具体的代码:

    def wrapper(func):   #生成器预激装饰器
        def inner():
            g = func()   #g = average_func()
            g.__next__()
            return g
        return inner
    
    @wrapper
    def average_func():  # average_func = wrapper(average_func) 返回inner, 执行 average_func(),相当于执行inner()
        total = 0
        count = 0
        average = 0
        while True:
            value = yield average  #0  30.0  25.0
            total += value  #30  50
            count += 1      #1   2
            average = total/count  #30  25
    gen = average_func()
    print(gen.send(30))
    # print(g.__next__())   #激活生成器print(gen.send(20))
    print(gen.send(10))

    此处代码是装饰器与生成器的结合使用,我们首先来分析函数的执行流程:1.首先执行函数average_func(),我们发现该函数被一个装饰器所修饰,按照执行流程,首先会去执行装饰器函数,通过观察,装饰器的主要作用是初始化生成器函数体的运行,具体而言是使用g.__next__()方法来驱动函数体的执行,直到遇到第一个yield才暂时让函数处于挂起的状态,此时装饰器函数返回一个生成器对象g,因为我们需要一个生成器对象g,所以在装饰器函数中需要return返回值。此时执行到gen = average_func()那么gen就是一个生成器对象,而且执行到这一步,我们也激活了生成器函数体的运行,此时函数状态暂时停止在yield average这里,接着我们执行

    print(gen.send(30)),按照我们前面的分析流程,value的值被赋值为30,接着往下走,total=30,count=1,average=30,由于没有遇到yield,我们接着执行while True,当我们再次执行到 value = yield average时,此时返回average的值给print(gen.send(30)),为30;然后我们接着执行下面的print(gen.send(20)),此时value的值变为20,total变为50,count变为2,average变为25...依此类推。直到执行完毕print(gen.send(10))所以上面函数的打印值为:30.0 30.0 26.666666666666668 22.5

    本题是生成器与装饰器的实际结合使用,希望大家能够仔细体会。


     综合上面几个综合案例的分析,我们可以总结出如下的结论:

    【001】send和next工作的起止位置是完全相同的,即两者都是遇到yield关键字,然后暂时是生成器函数体处于挂起的状态;
    【002】send可以把一个值作为信号量传递到函数中去,这就体现了send关键字的作用,它主要用于和外部生成器对象进行交互
    【003】在生成器执行伊始,只能先用next,因为我们需要使用next()方法激活生成器函数体去执行,然后遇到第一个yield,以方便后面使用send方法往生成器函数中
    传递参数
    【004】只要用send传递参数的时候,必须在生成器中还有一个未被返回的yield

    我们最后一起来看看一道比较牛逼精彩的面试题,先上代码:

    def add(n,i):
        return n+i
    def test():
        for i in range(4):
            yield i
    g=test()
    for n in [1,10,5]:
        g=(add(n,i) for i in g)
    print(list(g))

    遇到这样的题目,我们首先观察整个函数的特点,test()是一个生成器函数,而且题目中也出现了使用for循环来迭代遍历生成器函数。我们首先专注于解决外层的for循环,for n in [1,10,5],这里不是range,而是一个列表,由于改题目是循环嵌套循环,所以我们需要拆开求解,拆解步骤如下:

    [001]先执行这步:得到g
    n = 1
    g=(add(1,i) for i in g)
    
    [002]001执行完毕后,才开始执行002,此时002中的g是001中的g,因此我们将g=(add(10,i) for i in g)变为
    g=(add(10,i) for i in (add(1,i) for i in g))
    
    n=10
    g=(add(10,i) for i in g)
    
    [003]002执行完毕后,最后开始执行003,此时003中的g又是002中的g,因此我们将003中的g换为002中的g,即做如下的变化
    g=(add(5,i) for i in g)变为:g=(add(5,i) for i in (add(10,i) for i in (add(1,i) for i in g)))
    
    n=5
    g=(add(5,i) for i in g)
    
    print(list(g))

    经过上面步骤的拆解与分析,我们最终所求的表达式为:g=(add(5,i) for i in (add(10,i) for i in (add(1,i) for i in g)))

    针对这样复杂的嵌套表达式,我们的计算原则就是从里面逐层拆解,首先计算生成器表达式(add(1,i) for i in g),这里首先计算for i in g,相当于使用for循环遍历生成器对象g,执行完毕结果后为:0,1,2,3,此时接着执行add(1,i),i的值分别为0,1,2,3,执行完毕add(1,i)后,结果分别为1,2,3,4。

    接着我们来执行第二层嵌套循环add(10,i) for i in (1,2,3,4),执行完毕后的结果为:11,12,13,14,最后来执行外层的嵌套循环:add(5,i) for i in (11,12,13,14),结果为:16,17,18,19.

    题目的最后一行是将生成器对象里面的元素封装到列表中,然后打印列表,因此最终的结果是:16,17,18,19.

      咋一看,我们分析得头头是道,事实上结果是错误的!原因在于我们没有很好地理解生成器的延迟执行,其实开始分析的时候我们就已经错了,比如当n=1时,我们分解这一步,错误地认为生成器会将n=1带入到生成器表达式g=(add(n,i) for i in g)中,于是该生成器表达式的值就变为g=(add(1,i) for i in g),正是基于这样的推理,当后面n为10和5时,我们都将n的值带入到生成器表达式中,于是得到了最终的错误表达式:g=(add(5,i) for i in (add(10,i) for i in (add(1,i) for i in g)))。

      导致上面错误的原因在于,我们并没有很好地理解生成器的延迟执行,我们错误的认为每分解一步,就会将n的值带入到表达式中。其实对于生成器而言,根本就没有执行,因此也谈不上所谓的将n值带入进去了!只有在执行list(g)时,此时表明我们需要去生成器里面取值了,此时才开始计算,才开始将n的值带入到最终的生成器表达式中,因此最终的n值为5,前面n为1和10压根就没有什么作用,因为压根就没有执行,压根就没有执行赋值操作。所以正确的分解步骤如下所示:

    [001]先执行这步:得到g
    n = 1
    g=(add(n,i) for i in g)
    
    [002]001执行完毕后,才开始执行002,此时002中的g是001中的g,因此我们将g=(add(n,i) for i in g)变为
    g=(add(n,i) for i in (add(n,i) for i in g))
    
    n=10
    g=(add(n,i) for i in g)
    
    [003]002执行完毕后,最后开始执行003,此时003中的g又是002中的g,因此我们将003中的g换为002中的g,即做如下的变化
    g=(add(n,i) for i in g)变为:g=(add(n,i) for i in (add(n,i) for i in (add(n,i) for i in g)))
    
    n=5
    g=(add(n,i) for i in g)
    
    print(list(g))
    [004]只有在最后一步执行list(g)时,才真正驱动生成器的计算,才开始将n的值带入到表达式
    g=(add(n,i) for i in (add(n,i) for i in (add(n,i) for i in g)))中,所以最终计算的表达式中n的值为5,于是我们得到
    了正确的表达式:
    g=(add(5,i) for i in (add(5,i) for i in (add(5,i) for i in g)));

    而不是如下错误的表达式: g
    =(add(5,i) for i in (add(10,i) for i in (add(1,i) for i in g)))

    经过上面的分析,于是我们来计算生成器表达式的值:g=(add(5,i) for i in (add(5,i) for i in (add(5,i) for i in g))),同理和上面一样的分析,得到最终的结果值为15,16,17,18。

     下面来总结下整个题目要注意的点:

    1.首先是最外层的for循环,这里的n分别取三个值,因此for循环里面的内容会执行三次;记住外层的for循环只是控制里面生成器表达式执行的次数,而不是真正的对n进行赋值,真正影响n取值的是最后一个值,这才是起决定作用的。

    2.for循环里面的生成器表达式不断嵌套,对于这类计算需求,需要我们不断地进行拆解计算,从最里层开始计算。


    结语:

       以上就是关于send方法的相关探讨和剖析,重点需要掌握send方法的实际意义与使用!大家可以结合笔者列出的几道题目来体会下send方法的运用。下一篇笔者将详细分析一个运用函数生成器,装饰器相结合的实际案例,希望给能够带领大家掌握相关知识点的实际运用。最后推荐一篇不错的文章给大家:http://www.cnblogs.com/Eva-J/articles/7213953.html

  • 相关阅读:
    【原创】C# 文件操作详解(三)Directory类
    【原创】C# 文件操作详解(一)File类
    【原创】VS使用技巧——工欲善其事必先利其器
    【原创】C# 文件操作详解(四)DirectoryInfo类
    strpos用法
    调试跳转动态打印
    解决DIV超出样式长度自动换行
    PHP时间戳常用转换在(大、小月问题)
    懒人JS
    PHP 快速排序 与二维数组排序
  • 原文地址:https://www.cnblogs.com/pyspark/p/7486425.html
Copyright © 2011-2022 走看看