一场由yield引发的连串拷问
最近在学习Python中生成器时,遇到了一个yield关键词,廖雪峰老师的官网中也没有详细的解释,经过一番查阅和研究,终于对它有了一些认识并做了总结(如有不对之处,还请大神指正)。
首先先简单了解下生成器generator,它是为了弥补类似list生成序列时造成的内存空间浪费,例如下面代码中L会将所有值运算出来,全部放到内存中,可想而知,要是有百万千万级的数据,该占用多大内存。而使用生成器的形式,只要将[]改为(),这样只有需要用到的时候,才会去计算下一个值。
>>> L = [x * x for x in range(10)] >>> L [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] >>> g = (x * x for x in range(10)) >>> g <generator object <genexpr> at 0x0062B568>
如何取出g中的值,只要调用next()就可以了
>>> next(g) 0 >>> next(g) 1
接下来进入正题,我们通过一个实战来讲下yield关键字,让你完全攻克它
1 def foo(num): 2 print("foo starting") 3 while num < 5: 4 res = yield num 5 num = num + 1 6 print('res ::',res) 7 8 print("Starting First") 9 g = foo(0) 10 11 print("Starting for") 12 for n in g: 13 print('n ::',n)
首先先看下程序执行顺序,按照一般函数理解,3个starting的顺序应该是 Starting First --> foo starting --> Starting for,但执行结果如下
1 Starting First 2 Starting for 3 foo starting 4 n :: 0 5 res :: None 6 n :: 1 7 res :: None 8 n :: 2 9 res :: None 10 n :: 3 11 res :: None 12 n :: 4 13 res :: None
那就奇怪了,调用foo()函数动作在for循环之前啊,那么在这里要明确一下,函数中含有yield关键字,那么就不能把它当做一个普通函数了,而是一个生成器,且不会立即执行,只有调用next()时才会执行。
继续往下看,打印出“Starting for” 之后就进入了foo函数中执行了,也就是进入g生成器中了,可是上面讲过,只有调用next()才会到生成器中取值啊,为什么 for n in g 也会进去呢?
那就要研究下for n in g这段语句了,其实Python的for循环本质上就是通过不断调用next()函数实现的,也就是说如下两种写法是完全等价的。
1 listx = [1,2,3,4,5] 2 3 # 循环方法一 4 for n in listx: 5 print(n) 6 7 # 等价方法二 8 glistx = iter(listx) # 转换为Iterator对象 9 10 while True: 11 try: 12 # 获得下一个值: 13 x = next(glistx) 14 print(x) 15 except StopIteration: 16 # 遇到StopIteration就退出循环 17 break
这样就显而易见了,其实for循环中也是调了next()函数,到生成器中去取值的。
继续往下走,终于到了梦寐以求的yield关键字,怎么理解呢?首先可以先按照return来理解,所以当num=0的时候,走到yield num就直接return了,因此打印出 n :: 0
继续执行for循环,继续调用next()函数,那么这里就要了解一个知识点,生成器中的next()是接着上次return(yield)的地方继续执行,而不是从头执行,这就是yield与return的区别。此时,由于上一步yield已经返回了0到for循环了,那么res就没有变量来赋值了,也就是None,因此下一步打印的就是res :: None,此时num=num+1变为了1。在while True中循环,又遇到了yield num,那么又返回给了for函数一个1,因此 n接收到了1,打印出n :: 1,后面的以此类推,不再赘述。
根据上面步骤解读,应该已经明白了yield,next()的含义和用法了吧,那我们再乘胜追击,在for循环中再加个send()函数,这又是什么含义呢?
1 def foo(num): 2 print("foo starting") 3 while num < 5: 4 res = yield num 5 num = num + 1 6 print('res ::',res) 7 8 print("Starting First") 9 g = foo(0) 10 11 print("Starting for") 12 for n in g: 13 g.send(n + 100) 14 print('n ::',n)
其实可以这样理解,yield num返回之后,程序下一步是赋值给res,只不过yield返回后变为空了,所以res被赋值None,而send就是解决赋值的问题。send()含义就是继续执行yield之后的赋值操作,也就是重新赋值给res,那么再打印res的话就不是None了,上述执行结果如下:
1 Starting First 2 Starting for 3 foo starting 4 res :: 100 5 n :: 0 6 res :: None 7 res :: 102 8 n :: 2 9 res :: None 10 res :: 104 11 Traceback (most recent call last): 12 File "d:/VSCode/Python/genaratex.py", line 13, in <module> 13 g.send(n + 100) 14 StopIteration
按照如上思路,先赋值res=100,再打印res,再打印n。下个循环后res还是应该先赋值101啊,为什么结果是res :: None 和 res :: 102 呢,这里又有一个知识点,send函数中不仅仅是赋值,并且自带next()函数调用,所以在send内部就执行了一次next(),导致num再次加1,再次执行了yield num,只是for函数里面没有接收变量罢了,因此就会出现打印出res :: None和 res :: 102的输出了。
如此,代码最后的报错也应该知道为什么了吧,即当n获取生成器中已经是最后一个值的时候,send中再次next(),当然找不到值了,触发了StopIteration异常。
总结:
- 生成器generator可以用()来定义,也可以使用iter()来转换,带yield的函数默认都是generator
- next()取值时,除首次外都是从上次yield的地方开始执行的
- Python中for循环的本质也是调用next()来逐个取值
- send函数是先在yield处进行赋值,再进行next()操作,如果没有进行yield,直接调用send会报错找不到需要赋值的对象。