学习Scrapy的过程中碰到 yeild
这个关键字,我使用Python快半年了,还真的是第一次遇到这个关键字,于是我花了点时间研究后,终于明白它的作用了,怕下次看到时忘记,所以用这篇文将yield这个关键字重点整理一下。
1. yield的核心目的:为了节省记忆体
如果想要印出0~100的平方时,我们可能会这样写。
powers = [x**2 for x in range(100)]for x in powers: print(x)
但这样有一个致命问题在于,必须把整个list都存放在记忆体中,100个元素可能还不成问题,但如果今天的对象是一百万笔资料,记忆体可能会承受不了,程式就崩溃了。
接下来就会说明yield要如何节省记忆体,但在此之前,先来谈谈Python的生成器(generator)。
2. 什么是生成器(generator)?
生成器是一个可迭代的物件,可以放在for迴圈的in前面,或者使用next()函数呼叫执行下一次迭代。
和列表的差别在于,生成器会保存上次纪录,并只有在呼叫下一层迭代的时候才载入记忆体执行。
所以将上面的例子改写成生成器,结果是一样的,却可以防止超过记忆体,注意我用的是 (
而不是 [
。
powers = (x**2 for x in range(100))for x in powers: print(x)
3. 函数加入yield后不再是一般的函数,而被视作为生成器(generator)
呼叫函数后,回传的并非数值,而是函数的生成器物件。
4. yield和return一样会回传值,不过yield会记住上次执行的位置
yield和return一样都会回传值并中断在目前位置,但最大不同在于yield在下次迭代时会从上次迭代的下一行接续执行,一直执行到下一个yield出现,如果没有下一个yield则结束这个生成器。而且接续上一个迭代前的变数不会改变,就是维持上次结束前的模样。
这部分我们来看下面这个例子:
def yield_test(n): print("start n =", n) for i in range(n): yield i*i print("i =", i) print("end")tests = yield_test(5)for test in tests: print("test =", test) print("--------")
执行结果:
start n = 5test = 0--------i = 0test = 1--------i = 1test = 4--------i = 2test = 9--------i = 3test = 16--------i = 4end
从第10、11行看到呼叫yield_test()后回传的不是一个数值,而是一个可迭代的生成器。在第一次迭代时,印出了 "start n = 5",因为不在迴圈中,所以仅仅印出这一次。进入迴圈中,第一次时 i=0,接着遇到yield并回传 0*0 = 0,并回到主程序。主程序的test接收到回传的0,于是印出 "test = 0" 并印出 "--------",结束这次迭代。接着进行第二次迭代,会从上次结束的下一行开始,因此印出 "i = 0"。完成后又回到迴圈开始,这时 i=1,接着再次遇到yield并回传 1*1 = 1,并回到主程序。主程序的test接收到回传的1,于是印出 "test = 1" 并印出 "--------",结束这次迭代。其他次迭代依此类推,直到i=5跳出迴圈,印出 "end" 之后已经没有yield了,生成器会返回一个error StopIteration
(这边没有印出来),告诉主程序迭代已经结束了。结束主程序。看完上面例子后,应该会从原本朦朦胧胧到有点概念了吧,其实yield有点像侦错模式的中断点,只是多了中断时回传值而已。
5. next()呼叫下一次迭代,send(n)呼叫下一次迭代并传递参数
def test(): print("start...") while True: throw = yield 10 print("throw:", throw)p = test()print(next(p))print("-----------")print(next(p))print("-----------")print(g.send(7))print("-----------")
执行结果:
start...10-----------throw: None10-----------throw: 710-----------
建立一个可迭代生成器p。next()执行第一次迭代,印出 "start..." 并回传 10,但注意throw在赋予值之前就被中断了。next()执行第二次迭代,因为throw并没有被没有被赋予值,所以印出 "throw: None",接着回传 10。send()传入7,等同于在上次结束的位置填入7,因此 throw=7,印出 "throw: 7"。顺带一提,第一次迭代不可以send任何数值进去,因为没有上一个位置可以接收。
6. Python range小知识
在Python 2.X中,有分range和xrange两种,range就像第一个例子,生成一个[0, 1, 2, ...]的list。xrange则像第二种例子,使用生成器减少记忆体消耗。
但在Python 3.X后range就等于xrange,使用type()检查会知道已经是range型态了。
print(type(range(10))) # <class 'range'>
如果开始学就是Python3.X,就不必在意这些细节,继续放心地用range吧!