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

posted @ 2017-09-06 19:01  哀乐之巅写年华  阅读(890)  评论(0编辑  收藏  举报