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
Python学习之旅—生成器对象的send方法详解的更多相关文章
- python学习笔记8--面向对象--属性和方法详解
属性: 公有属性 (属于类,每个类一份) 普通属性 (属于对象,每个对象一份) 私有属性 (属于对象,跟普通属性相似,只是不能通过对象直接访问) 方法:(按作用) 构造方法 析构函数 方法: ...
- 【Python学习之十】yield之send方法
yield作用 简单地讲,yield 的作用就是把一个函数变成一个 generator,带有 yield 的函数不再是一个普通函数,Python 解释器会将其视为一个 generator.下面以斐波拉 ...
- Python学习之旅—生成器与迭代器案例剖析
前言 前面一篇博客笔者带大家详细探讨了生成器与迭代器的本质,本次我们将实际分析一个具体案例来加深对生成器与迭代器相关知识点的理解. 本次的案例是一个文件过滤操作,所做的主要操作就是过滤出一个目录下的文 ...
- python特性(八):生成器对象的send方法
生成器对象是一个迭代器.但是它比迭代器对象多了一些方法,它们包括send方法,throw方法和close方法.这些方法,主要是用于外部与生成器对象的交互.本文先介绍send方法. send方法有一个参 ...
- python(可迭代对象,迭代器,生成器及send方法详解)
一.可迭代对象 对象必须提供一个__iter__()方法,如果有,那么就是可迭代对象, 像列表,元祖,字典等都是可迭代对象可使用isinstance(obj,Iterable)方法判断 from co ...
- Python_List对象内置方法详解
目录 目录 前言 软件环境 列表List 修改列表的元素 插入列表元素 extend 将序列中的元素迭代的附加到list中 insert 在指定的索引号中插入一个元素 删除列表元素 del 删除Lis ...
- Python_序列对象内置方法详解_String
目录 目录 前言 软件环境 序列类型 序列的操作方法 索引调用 切片运算符 扩展切片运算符 序列元素的反转 连接操作符 重复运算符 成员关系符 序列内置方法 len 获取序列对象的长度 zip 混合两 ...
- JavaScript原生对象属性和方法详解——Array对象
http://www.feeldesignstudio.com/2013/09/native-javascript-object-properties-and-methods-array/ lengt ...
- 闭包在python中的应用,translate和maketrans方法详解
python对字符串的处理是比较高效的,方法很多.maketrans和translate两个方法被应用的很多,但是具体怎么用常常想不起来. 让我们先回顾下这两个方法吧: 1.s.translate(t ...
随机推荐
- Android Studio如何Format代码
Android Studio如何Format代码 Reformat code Shift + CTRL + ALT + L (Win) OPTION + CMD + L (Mac)
- BUPT复试专题—三元组(2016)
题目描述 给你一个长度为m的数组(数组元素从0到m-1),如果数组里有a[i]+a[j]==a[k](i,j,k大于等于0并且小于m),便称之为三元组.现在给你一个数组,让你求三元组的个数. 例如m为 ...
- 编资源bundle时图片文件变成tiff的解决方法
一般,编写SDK的时候,如果SDK还带了一些资源文件,那么最理想的是将资源文件也打包成为bundle给应用方一起使用.而在编资源bundle时,有时会发现编译好后的图片文件从png转成了tiff,这样 ...
- jquery 获取下拉框 某个text='xxx'的option的属性 非选中 如何获得select被选中option的value和text和......
jquery 获取下拉框 某个text='xxx'的option的属性 非选中 5 jquery 获取下拉框 text='1'的 option 的value 属性值 我写的var t= $(" ...
- C3P0连接池配置和实现详解(转)
一.配置 <c3p0-config> <default-config> <!--当连接池中的连接耗尽的时候c3p0一次同时获取的连接数.Default: 3 --> ...
- 项目Beta冲刺(团队4/7)
项目Beta冲刺(团队4/7) 团队名称: 云打印 作业要求: 项目Beta冲刺(团队) 作业目标: 完成项目Beta版本 团队队员 队员学号 队员姓名 个人博客地址 备注 221600412 陈宇 ...
- linux下提示command not found
首先就要考虑root 的$PATH里是否已经包含了这些环境变量. 主要是这四个:/bin ,/usr/bin,/sbin,/usr/sbin. 四个主要存放的东东: ./bin: bin为binary ...
- mac上pydev
转自:http://m.blog.csdn.net/blog/yangfu132/23689823 本来网上有教程,但是往往又一些不周到的地方,让人走了不少弯路. 使用 PyDev 进行调试 第一步: ...
- Python调用C/Fortran混合的动态链接库--中篇
接下来,介绍一个简单的例子,从fortran中传递并返回一维自定义结构体数组到python注意点:1.fortran新标准支持可分配数组作为变量传入并在subroutine或function分配后返回 ...
- Apache Qpid Broker的安全机制
一. Apache Qpid的安全机制简介 Apache Qpid提供多种安全机制,包括用户认证.规则定制的授权.消息加密和数字签名等.Apache Qpid使用SASL框架实现对用户身份的认 ...