一場由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()就能夠了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異常。

總結:

  1. 生成器generator能夠用()來定義,也可使用iter()來轉換,帶yield的函數默認都是generator
  2. next()取值時,除首次外都是從上次yield的地方開始執行的
  3. Python中for循環的本質也是調用next()來逐個取值
  4. send函數是先在yield處進行賦值,再進行next()操做,若是沒有進行yield,直接調用send會報錯找不到須要賦值的對象。
相關文章
相關標籤/搜索