最近在學習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()就能夠了spa
>>> next(g) 0 >>> next(g) 1
接下來進入正題,咱們經過一個實戰來說下yield關鍵字,讓你徹底攻克它code
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()時纔會執行。blog
繼續往下看,打印出「Starting for」 以後就進入了foo函數中執行了,也就是進入g生成器中了,但是上面講過,只有調用next()纔會到生成器中取值啊,爲何 for n in g 也會進去呢?內存
那就要研究下for n in g這段語句了,其實Python的for循環本質上就是經過不斷調用next()函數實現的,也就是說以下兩種寫法是徹底等價的。generator
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()函數,到生成器中去取值的。it
繼續往下走,終於到了求之不得的yield關鍵字,怎麼理解呢?首先能夠先按照return來理解,因此當num=0的時候,走到yield num就直接return了,所以打印出 n :: 0io
繼續執行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異常。
總結: