http://kaito-kidd.com/2018/05/21/python-advance-yield/#more
yield關鍵字在Python中開發中使用較為頻繁,它為我們某些開發場景提供了便利,這篇文章我們來深入講解yield相關知識。
在講yield之前,我們先複習一下疊代器與生成器的差別,可以參考我之前寫的文章:Python技術進階——疊代器、可疊代對象、生成器。
簡單總結如下:
實作了疊代器協定iter和next/next方法的對象被稱作疊代器
疊代器可以使用for執行輸出每個元素
生成器是一種特殊的疊代器
一個函數内,如果包含了yield關鍵字,這個函數就是一個生成器。
注意,在執行g = gen(5)時,函數中的代碼并沒有執行,此時我們隻是建立了一個生成器對象,他的類型是generator。
當執行for i in g時,每執行一次循環,直到執行到yield時,傳回yield後面的值。
換句話說,我們想輸出5個元素,在建立生成器時,這個5個元素此時并沒有産生,什麼時候産生呢?在執行for循環遇到yield時,此時才會逐個生成每個元素。
生成器除了實作疊代器協定可以進行疊代之外,還包含一些方法:
generator.next():每次執行到遇到yield後傳回,直到沒有yield,抛出StopIterator異常
generator.send(value):将yield的值設定為value
generator.throw(type[, value[, traceback]]):向生成器目前狀态抛出一個異常
generator.close():關閉生成器
為了更便于你了解隻有在遇到yield時才産生值,我們可以改寫程式如下:
隻有在執行g.next()時,才會産生值,并且生成器會保留上下文資訊,在再次執行g.next()時繼續傳回。
上面的例子隻展示了在yield後有值的情況,其實也可以使用j = yield i這種文法,我們看下面的代碼:
如果我們執行:
這個生成器函數相當于無限生成每次翻倍的數字,一直循環下去,直到我們殺死程序才能停止。
在上面的代碼你會發現,貌似永遠執行不到j == -1這個分支裡,如果想讓代碼執行到這,如何做?
這裡就要用到生成的send方法,它可以在外部傳入一個值,使得改變生成器目前的狀态。
執行g.send(-1),相當于把-1傳入生成器,指派給了yield之前的j,進而改變了生成器内部的執行狀态。
除了可以向生成器内部傳入指定值,還可以傳入指定異常:
throw與next類似,但是以傳入異常的方式使生成器執行,throw一般在開發中很少被用到。
上面簡單介紹了生成器和yield的使用方式,那麼yield一般在哪些場景中被使用?
如果你想生成一個非常大的清單,使用list時隻能一次性在記憶體中建立出這個清單,這可能導緻記憶體資源申請非常大,甚至有可能被作業系統殺死程序。
直接在記憶體中生成一個大清單:
由于生成器隻有在執行到yield時才會産生值,我們可以使用這個特性優雅地解決這類問題:
如果一個函數中要産生一個清單,但這個清單可能是多個邏輯塊組合後才能産生的,這就會導緻我們的代碼結構變得複雜:
使用yield生成這個清單:
我們看到,在第一個例子中,我們隻能先聲明一個list類型的變量,然後在每個邏輯塊中産生元素,之後append到結果中,最終return傳回這個結果。
而使用yield後,隻需在每個邏輯塊需要産生并傳回元素時,使用yield即可,代碼更加簡潔,結構更清晰,同時還擁有減少記憶體占用的好處。
我們都比較熟悉程序、線程,一般為了提高程式的運作效率,會使用多程序、多線程進行開發,最常用的程式設計模型就是生産者-消費者模型,即一個程序/線程生産資料,其他程序/線程消費資料。
在多程序、多線程開發時,為了防止資源被篡改,往往會進行加鎖,這就導緻了程式設計的複雜程度。
在Python開發中,也提供了多程序和多線程的開發方式,但由于解釋器GIL的存在,多線程開發并不能提高執行效率。是以在Python中,更多提高執行效率的程式設計模型是:協程。
什麼是協程?簡單來說,由多個程式塊組合協作執行的程式,稱之為協程。可能這麼說還是太過模糊,我們用yield實作一個生産者-消費者的例子:
整個程式執行流程如下:
c = consumer()建立一個生成器對象
producer(c)開始執行代碼,c.next()會啟動生成器consumer直到代碼運作到j = yield i處,此時consumer第一次執行完畢,傳回
producer函數繼續向下執行,直到c.send(i),利用生成器的send方法,向consumer發送資料
consumer函數被喚醒,從j = yield i處開始執行,并接收producer傳來的資料指派給j,然後列印輸出,直到再次執行到yield處,傳回
producer繼續執行循環,執行上面的過程,逐個發送資料給cosnumer,直到循環結束
最終c.close()關閉consumer生成器,程式退出
在上面的代碼中我們發現,程式運作時,在producer和consumer這2個函數之間來回切換執行,完成了生産任務、消費任務的場景,而且整個程式運作在單程序單線程下。
這其中的原理就是利用了生成器的yield關鍵字以及生成器的next和send方法。
這麼做的好處在于:
整個程式運作過程中無鎖,程式設計複雜度降低
程式在函數之間來回切換,是在使用者态下進行的,不像程序/線程切換陷入核心狀态,沒有核心态的上下文切換,損耗更小,執行效率更高
Python的生成器實作了協程的程式設計方式,為程式的并發執行提供了程式設計基礎。
Python的很多第三方包都是基于這一特性進行封裝的,例如gevent、tornado,它們都大大提高了程式的運作效率。
這篇文章主要講了Python中生成器與yield的相關知識,總結如下:
生成器在生成很大的清單的場景,能夠節省記憶體空間的占用
在複雜邏輯塊生成清單元素時,使用yield能極大簡化代碼結構
生成器的特性為Python的并發程式設計模型——協程,提供了程式設計基礎
如果此文章能給您帶來小小的工作效率提升,不妨小額贊助我一下,以鼓勵我寫出更好的文章!