Python 中 yield 的用法詳解

Python 中 yield 的用法詳解

前言

yield 最主要的目的與功能是 :

為了節省記憶體的使用。

讓我們來看看下面一個狀況:

my_list = [x*x for x in range(1000000000000000000)]
for i in my_list :
...    print(i)

可以看到,通常我們在進行遞迴時都會傾向於先把要遞迴的 list 存起來,之後再以 for 迴圈把內容逐步輸出。

但我們可以看到 my_list 裡有非常驚人的數列,因此如果要全部存進記憶體,運行效率就會變的非常的慢。

這時可能有人會說,那為什麼不直接用 range 就好呢?其實 range 在 Python 2 是分成 range xrange 兩個,range 的生成方式就像上面的 my_list;xrange 才是以 class 的型態運行。

其中要注意的是python3時已經沒有xrange()了,在python3中,range()就是xrange()了,你可以在python3中查看range()的類型,它已經是個<class 'range'>了,而不是一個list了。

這個問題,最常被注意到的便是爬蟲。有時候短短的幾千條網站,還能靠記憶體來爬;但如果有數十萬個網站,每一個網站都以 list 的方式儲存,記憶體不夠大,程式就會直接崩潰了。

因此 yield 設計來的目的,就是為了單次輸出內容。我們可以把 yield 暫時看成 return,但是這個 return 的功能只有單次。而且,一旦我們的程式執行到 yield 後,程式就會把值丟出,並暫時停止。

直到下一次的遞迴,程式才會從 yield 的下一行開始執行。那哪裡有遞迴呢?沒錯,最常被用到 for 迴圈裡,而且 for 迴圈自帶 next() 的功能。換句話說,for 迴圈會自動在程式內部進行下一輪的遞迴,因而觸動 yield 進行下一輪吐值。而所有能被迭代的物件,都能夠被 yield 製作成生成器。

好了,如果你對這些不明白的話,那先把 yield 看做 return ,之後再把它看做一個是生成器 generator 的一部分,然後直接看下面的範例 ,你就會明白yield的全部意思了:

範例 pythonYield-01.py:  Python 中 yield 的用法 。

# pythonYield-01.py
def foo():
  print("Starting...")
  while True:
    result = yield 4
    print("結果是 : ",result)
g = foo()
print(next(g))    
print("*"*20)
print(next(g))

執行結果

Starting...
4
********************
結果是 :  None
4

我直接解釋代碼運行順序,相當於代碼單步調試:

  • 1.程序開始執行以後,因為 foo 函數中有 yield 關鍵字,所以 foo 函數並不會真的執行,而是先得到一個 生成器g (相當於一個物件)。
  • 2.直到調用 next 方法,foo 函數正式開始執行,先執行 foo 函數中的 print方法,然後進入while 循環。
  • 3.程序遇到 yield 關鍵字,然後把 yield 想想成 returnreturn了一個 4 之後,程序停止,並沒有執行賦值給 result 操作,此時 next(g) 語句執行完成,所以輸出的前兩行(第一個是 while上面的 print 的結果,第二個是 return 出的結果)是執行 print(next(g)) 的結果。
  • 4.程序執行 print("*"*20),輸出20個*。
  • 5.又開始執行下面的 print(next(g)),這個時候和上面那個差不多,不過不同的是,這個時候是從剛才那個 next 程序停止的地方開始執行的,也就是要執行 result 的賦值操作,這時候要注意,這個時候賦值操作的右邊是沒有值的(因為剛才那個是 return 出去了,並沒有給賦值操作的左邊傳參數),所以這個時候 result 賦值是None,所以接著下面的輸出就是 結果是 :  None。
  • 6.程序會繼續在 while 裡執行,又一次碰到 yield,這個時候同樣 return 出 4,然後程序停止,print 函數輸出的 4 就是這次 return 出的4。

到這裡你可能就明白 yield return 的關係和區別了,帶 yield 的函數是一個生成器,而不是一個函數了,這個生成器有一個函數就是 next 函數,next 就相當於 “下一步” 生成哪個數,這一次的 next 開始的地方是接著上一次的 next 停止的地方執行的,所以調用 next 的時候,生成器並不會從 foo 函數的開始執行,只是接著上一步停止的地方開始,然後遇到 yield 後,return 出要生成的數,此步就結束。

範例 pythonYield-02.py:  Python 中 yield 的用法,再看一個這個生成器的 send 函數的例子,這個例子就把上面那個例子的最後一行換掉了。

# pythonYield-02.py
def foo():
  print("Starting...")
  while True:
    result = yield 4
    print("結果是 : ",result)
g = foo()
print(next(g))    
print("*"*20)
print(g.send(7))

執行結果

Starting...
4
********************
結果是 :  7
4

先大致說一下 send 函數的概念:上面那個 result 的值為什麼是 None,這個變成了 7,到底為什麼 ? 這是因為,send 是發送一個參數給 result 的,因為上面講到,return 的時候,並沒有把 4 賦值給 result,下次執行的時候只好繼續執行賦值操作,只好賦值為 None 了,而如果用 send 的話,開始執行的時候,先接著上一次(return 4之後)執行,先把 7 賦值給了result,然後執行next 的作用,遇見下一回的 yield,return 出結果後結束。

  • 程序執行 g.send(7),程序會從 yield 關鍵字那一行繼續向下運行,send 會把 7 這個值賦值給 result 變數。
  • 由於 send 方法中包含 next() 方法,所以程序會繼續向下運行執行 print 方法,然後再次進入 while 循環。
  • 程序執行再次遇到 yield 關鍵字,yield 會返回後面的值後,程序再次暫停,直到再次調用next 方法或 send 方法。


參考資料

特色、摘要,Feature、Summary:

關鍵字、標籤,Keyword、Tag:

  • Python,Python-Tutorial,

留言

這個網誌中的熱門文章

Ubuntu 常用指令、分類與簡介

iptables的觀念與使用

網路設定必要參數IP、netmask(遮罩)、Gateway(閘道)、DNS

了解、分析登錄檔 - log

Python 與SQLite 資料庫

Blogger文章排版範本

Pandas 模組

如何撰寫Shell Script

查詢指令或設定 -Linux 線上手冊 - man

下載網頁使用 requests 模組