首頁  >  文章  >  後端開發  >  Python yield 使用淺析

Python yield 使用淺析

高洛峰
高洛峰原創
2016-10-19 16:04:391095瀏覽

您可能聽說過,帶有 yield 的函數在 Python 中被稱為 generator(生成器),何謂 generator ?

  我們先拋開 generator,以一個常見的程式設計題目來展示 yield 的概念。

  如何產生斐波那契數列

  斐波那契(Fibonacci)數列是一個非常簡單的遞歸數列,除第一個和第二個數外,任一個數都可由前兩個數相加得到。用電腦程式輸出斐波那契數列的前N 個數是一個非常簡單的問題,許多初學者都可以輕易寫出如下函數:

  清單1. 簡單輸出斐波那契數列前N 個數

deffab(max):
   n, a, b =0, 0, 1
   whilen < max:
       printb
       a, b =b, a +b
       n =n +1

  執行fab(5),我們可以得到以下輸出:

>>> fab(5)

  結果沒有問題,但有經驗的開發者會指出,直接在fab 函數中用print 列印數字會導致該函數可重複使用性較差,因為fab 函數傳回None,其他函數無法取得該函數產生的數列。

  要提高 fab 函數的可重複使用性,最好不要直接列印出數列,而是傳回一個 List。以下是fab 函數改寫後的第二個版本:

  清單2. 輸出斐波那契數列前N 個數第二版

deffab(max):
   n, a, b =0, 0, 1
   L =[]
   whilen < max:
       L.append(b)
       a, b =b, a +b
       n =n +1
   returnL

  可以使用以下方式列印出fab 函數傳回的List:

>> forn infab(5):

...     printn

...

  改寫後的fab 函數透過返回List 能滿足復用性的要求,但是更有經驗的開發者會指出,該函數在運行中佔用的記憶體會隨著參數max 的增加而增加,如果要控制記憶體佔用,最好不要用List

  來保存中間結果,而是透過iterable 物件來迭代。例如,在Python2.x 中,程式碼:

  清單3. 透過iterable 物件來迭代

fori inrange(1000): pass

  會導致產生一個1000 個元素的List,而程式碼:

  會導致產生一個1000 個元素的List,而程式碼:

  ): pass

  則不會產生一個1000 個元素的List,而是在每次迭代中傳回下一個數值,記憶體空間佔用量很小。因為 xrange 不回傳 List,而是回傳一個 iterable 物件。

  利用iterable 我們可以把fab 函數改寫為一個支援iterable 的class,以下是第三個版本的Fab:

  清單4. 第三個版本

classFab(object):
    
   def__init__(self, max):
       self.max=max
       self.n, self.a, self.b =0, 0, 1
    
   def__iter__(self):
       returnself
    
   defnext(self):
       ifself.n < self.max:
           r =self.b
           self.a, self.b =self.b, self.a +self.b
           self.n =self.n +1
           returnr
       raiseStopIteration()

  清單4. 第三個版本

deffab(max):
    n, a, b =0, 0, 1
    whilen < max:
        yieldb
        # print b
        a, b =b, a +b
        n =n +1

  清單4. 第三個版本

defread_file(fpath):
   BLOCK_SIZE =1024
   with open(fpath, &#39;rb&#39;) as f:
       whileTrue:
           block =f.read(BLOCK_SIZE)
           ifblock:
               yieldblock
           else:
               return

  Fab 類通過下一個列數數,記憶體佔用總是常數:

>>> forn inFab(5):

...     printn

...

  然而,使用class 改寫的這個版本,程式碼遠沒有第一版的程式碼遠函數來得簡潔。如果我們想要保持第一版fab 函數的簡潔性,同時又要獲得iterable 的效果,yield 就派上用場了:

  清單5. 使用yield 的第四版

rrreee

  清單5. 使用yield 的第四版

rrreee

  第四個版本的fab 和第四個版本的fab 和第四個版本第一版相比,光是把print b 改為了yield b,就在保持簡潔性的同時獲得了iterable 的效果。

  調用第四版的fab 和第二版的fab 完全一致:

>>> forn infab(5):

...     printn

一個函數變成一個generator,帶有yield 的函數不再是一個普通函數,Python 解釋器會將其視為一個generator,呼叫fab(5) 不會執行fab 函數,而是傳回一個iterable 物件!當for 迴圈執行時,每次迴圈都會執行fab 函數內部的程式碼,執行到yield b 時,fab 函數會傳回一個迭代值,下次迭代時,程式碼從yield b 的下一語句繼續執行,而函數的本地變數看起來和上次中斷執行前是完全一樣的,所以函數繼續執行,直到再次遇到yield。

  也可以手動呼叫fab(5) 的next() 方法(因為fab(5) 是一個generator 對象,該對象具有next() 方法),這樣我們就可以更清楚地看到fab 的執行流程:

  清單6. 執行流程

>>> f =fab(5)

>>> f.next()

>>> f.next()

>>> f.n

>>> f.next()

>>> f.next()> >> f.next()

>>> f.next()

>>> f.next()

Traceback (most recent call last):

File"", line 1, in StopStopIteration當函數執行結束時,generator 會自動拋出StopIteration 異常,表示迭代完成。在 for 迴圈裡,無需處理 StopIteration 異常,迴圈會正常結束。 🎜🎜  我們可以得出以下結論:🎜🎜  一個帶有yield 的函數就是一個generator,它和普通函數不同,產生一個generator 看起來像函數調用,但不會執行任何函數代碼,直到對其調用next( )(在for 迴圈中會自動呼叫next())才開始執行。雖然執行流程仍按函數的流程執行,但每執行到一個 yield 語句就會中斷,並傳回一個迭代值,下次執行時從 yield 的下一個語句繼續執行。看起來好像一個函數在正常執行的過程中被 yield 中斷了數次,每次中斷都會透過 yield 傳回目前的迭代值。 🎜

  yield 的好处是显而易见的,把一个函数改写为一个 generator 就获得了迭代能力,比起用类的实例保存状态来计算下一个 next() 的值,不仅代码简洁,而且执行流程异常清晰。

  如何判断一个函数是否是一个特殊的 generator 函数?可以利用 isgeneratorfunction 判断:

  清单 7. 使用 isgeneratorfunction 判断

>>> frominspect importisgeneratorfunction

>>> isgeneratorfunction(fab)

True

  要注意区分 fab 和 fab(5),fab 是一个 generator function,而 fab(5) 是调用 fab 返回的一个 generator,好比类的定义和类的实例的区别:

  清单 8. 类的定义和类的实例

>>> importtypes

>>> isinstance(fab, types.GeneratorType)

False

>>> isinstance(fab(5), types.GeneratorType)

True

  fab 是无法迭代的,而 fab(5) 是可迭代的:

>>> fromcollections importIterable

>>> isinstance(fab, Iterable)

False

>>> isinstance(fab(5), Iterable)

True

  每次调用 fab 函数都会生成一个新的 generator 实例,各实例互不影响:

>>> f1 =fab(3)

>>> f2 =fab(5)

>>> print'f1:', f1.next()

f1: 1

>>> print'f2:', f2.next()

f2: 1

>>> print'f1:', f1.next()

f1: 1

>>> print'f2:', f2.next()

f2: 1

>>> print'f1:', f1.next()

f1: 2

>>> print'f2:', f2.next()

f2: 2

>>> print'f2:', f2.next()

f2: 3

>>> print'f2:', f2.next()

f2: 5

  return 的作用

  在一个 generator function 中,如果没有 return,则默认执行至函数完毕,如果在执行过程中 return,则直接抛出 StopIteration 终止迭代。

  另一个例子

  另一个 yield 的例子来源于文件读取。如果直接对文件对象调用 read() 方法,会导致不可预测的内存占用。好的方法是利用固定长度的缓冲区来不断读取文件内容。通过 yield,我们不再需要编写读文件的迭代类,就可以轻松实现文件读取:

  清单 9. 另一个 yield 的例子

defread_file(fpath):
   BLOCK_SIZE =1024
   with open(fpath, &#39;rb&#39;) as f:
       whileTrue:
           block =f.read(BLOCK_SIZE)
           ifblock:
               yieldblock
           else:
               return

  以上仅仅简单介绍了 yield 的基本概念和用法,yield 在 Python 3 中还有更强大的用法,我们会在后续文章中讨论。

  注:本文的代码均在 Python 2.7 中调试通过


陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn