本文目錄一覽:
閑話python 45: 淺談生成器yield
生成器似乎並不是一個經常被開發者討論的語法,因此也就沒有它的大兄弟迭代器那麼著名。大家不討論它並不是說大家都已經對它熟悉到人盡皆知,與之相反,即使是工作多年的開發者可能對生成器的運行過程還是知之甚少。這是什麼原因導致的呢?我猜想大概有以下幾點原因: (1)運行流程不同尋常,(2)日常開發不需要,(3)常常將生成器與迭代器混淆。 生成器的運行流程可以按照協程來理解,也就是說 返回中間結果,斷點繼續運行 。這與我們通常對於程序調用的理解稍有差異。這種運行模式是針對什麼樣的需求呢? 一般而言,生成器是應用於大量磁碟資源的處理。 比如一個很大的文件,每次讀取一行,下一次讀取需要以上一次讀取的位置為基礎。下面就通過代碼演示具體看看生成器的運行機制、使用方式以及與迭代器的比較。
什麼是生成器?直接用文字描述可能太過抽象,倒不如先運行一段代碼,分析這段代碼的運行流程,然後總結出自己對生成器的理解。
從以上演示可以看出,這段代碼定義了一個函數,這個函數除了yield這個關鍵字之外與一般函數並沒有差異,也就是說生成器的魔法都是這個yield關鍵字引起的。 第一點,函數的返回值是一個生成器對象。 上述代碼中,直接調用這個看似普通的函數,然後將返回值列印出來,發現返回值是一個對象,而並不是普通函數的返回值。 第二點,可以使用next對這個生成器對象進行操作 。生成器對象天然的可以被next函數調用,然後返回在yield關鍵字後面的內容。 第三,再次調用next函數處理生成器對象,發現是從上次yield語句之後繼續運行,直到下一個yield語句返回。
生成器的運行流程確實詭異,下面還要演示一個生成器可以執行的更加詭異的操作:運行過程中向函數傳參。
返回生成器和next函數操作生成器已經並不奇怪了,但是在函數運行過程中向其傳參還是讓人驚呆了。 調用生成器的send函數傳入參數,在函數內使用yield語句的返回值接收,然後繼續運行直到下一個yield語句返回。 以前實現這種運行流程的方式是在函數中加上一個從控制台獲取數據的指令,或者提前將參數傳入,但是現在不用了,send方式使得傳入的參數可以隨著讀取到的參數變化而變化。
很多的開發者比較容易混淆生成器和迭代器,而迭代器的運行過程更加符合一般的程序調用運行流程,因此從親進度和使用熟悉度而言,大家對迭代器更有好感。比如下面演示一個對迭代器使用next方法進行操作。
從以上演示來看,大家或許會認為迭代器比生成器簡單易用得太多了。不過,如果你了解迭代器的實現機制,可能就不會這麼早下結論了。python內置了一些已經實現了的迭代器使用確實方便,但是如果需要自己去寫一個迭代器呢?下面這段代碼就帶大家見識以下迭代器的實現。
在python中,能被next函數操作的對象一定帶有__next__函數的實現,而能夠被迭代的對象有必須實現__iter__函數。看了這麼一段操作,相信大家對迭代器實現的繁瑣也是深有體會了,那麼生成器的實現是不是會讓你覺得更加簡單易用呢?不過千萬別產生一個誤區,即生成器比迭代器簡單就多用生成器。 在實際開發中,如果遇到與大量磁碟文件或者資料庫操作相關的倒是可以使用生成器。但是在其他的任務中使用生成器難免有炫技,並且使邏輯不清晰而導致可讀性下降的嫌疑。 這大概也能解釋生成器受冷落的原因。不過作為一個專業的開發者,熟悉語言特性是分內之事。
到此,關於生成器的討論就結束了。本文的notebook版本文件在github上的cnbluegeek/notebook倉庫中共享,歡迎感興趣的朋友前往下載。
學 Python 怎能不知 yield?
理解yield 的 generator 概念,首先以一個常見的編程題目來展示 yield 的概念。
斐波那契(Fibonacci)數列是一個非常簡單的遞歸數列,除第一個和第二個數外,任意一個數都可由前兩個數相加得到。用計算機程序輸出斐波那契數列的前 N 個數是一個非常簡單的問題,有些 Python 基礎的小夥伴都可以輕易寫出如下函數:
第 1 版本:簡單輸出斐波那契數列前 N 個數
執行以上代碼,我們可以得到如下輸出:
輸出結果是沒有問題的,但是版本 1 中的寫法是直接在 createNum 函數中用 print 列印數字會導致該函數可復用性較差,因為 createNum 函數返回 None,其他函數無法獲得該函數生成的數列。
要提高 createNum 函數的可復用性,最好不要直接列印出數列,而是返回一個 List。以下是 createNum 函數改寫後的第二個版本:
第 2 版本:輸出斐波那契數列前 N 個數
該版本中 createNum 函數返回的 List的結果如下:
改寫後的 createNum 函數通過返回 List 能滿足復用性的要求,但是與此同時也會存在一個明顯的問題是:該函數在運行中佔用的內存會隨著參數 count 的增大而增大,如果要控制內存佔用,最好不要用 List 來保存中間結果,而是通過 iterable 對象來迭代。在每次迭代中返回下一個數值,如此:內存空間佔用很小。因為是直接返回一個 iterable 對象。
第 3 版本:使用 yield 輸出斐波那契數列前 N 個數
也可以手動調用 createNum(5) 的 next() 方法(因為 createNum(5) 是一個 generator 對象,該對象具有 next() 方法),這樣我們就可以更清楚地看到 createNum 的執行流程:
第 4 版本:執行流程
運行以上代碼,結果輸出如下:
由輸出結果可發現在執行第 6 個 print(next(num)) 時拋出一個 StopIteration 的異常,是因為在第 5 個 print(next(num)) 執行完時函數已經結束,再執行第 6 個print(next(num))時,generator 自動拋出 StopIteration 異常,表示迭代完成。在 for 循環里,無需處理 StopIteration 異常,循環會正常結束。
簡單地講,yield 的作用就是把一個函數變成一個 generator,帶有 yield 的函數不再是一個普通函數,Python 解釋器會將其視為一個 generator,調用 createNum(5) 不會執行 createNum 函數,而是返回一個 iterable 對象!
在 for 循環執行時,每次循環都會執行 createNum 函數內部的代碼,執行到 yield b 時,createNum 函數就會返回一個迭代值,下次迭代時,代碼從 yield b 的下一條語句繼續執行,而函數的本地變數看起來和上次中斷執行前是完全一樣的,於是函數繼續執行,直到再次遇到 yield。
一個帶有 yield 的函數就是一個 generator,它和普通函數不同,生成一個 generator 看起來像函數調用,但不會執行任何函數代碼,直到對其調用 next()(在 for 循環中會自動調用 next())才開始執行。雖然執行流程仍按函數的流程執行,但每執行到一個 yield 語句就會中斷,並返回一個迭代值,下次執行時從 yield 的下一個語句繼續執行。看起來就好像一個函數在正常執行的過程中被 yield 中斷了數次,每次中斷都會通過 yield 返回當前的迭代值。
yield 的好處是顯而易見的,把一個函數改寫為一個 generator 就獲得了迭代能力,比起用類的實例保存狀態來計算下一個 next() 的值,不僅代碼簡潔,而且執行流程異常清晰。
如何理解Python關鍵字yield
當函數被調用時,函數體中的代碼是不會運行的,函數僅僅是返回一個生成器對象。這裡理解起來可能稍微有點複雜。
函數中的代碼每次會在 for循環中被執行,接下來是最難的一部分:
for第一次調用生成器對象時,代碼將會從函數的開始處運行直到遇到 yield為止,然後返回此次循環的第一個值,接著循環地執行函數體,返回下一個值,直到沒有值返回為止。
Python yield關鍵字實現生產者和消費者
通常 yield 用在函數中表示這個函數被定義為一個生成器,可以通過 for 循環去遍歷這個生成器對象得到 yield 的值,如下:
yield 除了可以返回值,還能接收調用者傳參,下面以 廖雪峰博客
的生產者和消費者為例說明:
若在 c = consumer() 前面加上斷點調試可知程序的執行順序為
原創文章,作者:小藍,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/291064.html