您可能聽說過,帶有 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()
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, 'rb') 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, 'rb') as f: whileTrue: block =f.read(BLOCK_SIZE) ifblock: yieldblock else: return
以上仅仅简单介绍了 yield 的基本概念和用法,yield 在 Python 3 中还有更强大的用法,我们会在后续文章中讨论。
注:本文的代码均在 Python 2.7 中调试通过