相關知識點
生成器
帶有 yield 關鍵字的的函式在 Python 中被稱之為 generator(生成器)。Python 直譯器會將帶有 yield 關鍵字的函式視為一個 generator 來處理。一個函式或者子程式都只能 return 一次,但是一個生成器能暫停執行並返回一箇中間的結果 —— 這就是 yield 語句的功能 : 返回一箇中間值給呼叫者並暫停執行。
EXAMPLE:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
In [94]: def fab(max): ...: n, a, b = 0, 0, 1 ...: while n < max: ...: yield b ...: a, b = b, a + b ...: n = n + 1 ...: In [95]: f = fab(5) In [96]: f.next() Out[96]: 1 In [97]: f.next() Out[97]: 1 In [98]: f.next() Out[98]: 2 In [99]: f.next() Out[99]: 3 In [100]: f.next() Out[100]: 5 In [101]: f.next() --------------------------------------------------------------------------- StopIteration Traceback (most recent call last) <ipython-input-101-c3e65e5362fb> in <module>() ----> 1 f.next() StopIteration: |
生成器 fab()
的執行過程
執行語句 f = fab(5)
時,並不會馬上執行 fab()
函式的程式碼塊,而是首先返回一個 iterable 物件!
在 for 迴圈語句執行時,才會執行 fab()
函式的程式碼塊。
執行到語句 yield b
時,fab()
函式會返回一個迭代值,直到下次迭代前,程式流會回到 yield b
的下一條語句繼續執行,然後再次回到 for 迴圈,如此迭代直到結束。看起來就好像一個函式在正常執行的過程中被 yield
中斷了數次,每次中斷都會通過 yield
返回當前的迭代值。
由此可以看出,生成器通過關鍵字 yield
不斷的將迭代器返回到記憶體進行處理,而不會一次性的將物件全部放入記憶體,從而節省記憶體空間。從這點看來生成器和迭代器非常相似,但如果更深入的瞭解的話,其實兩者仍存在區別。
生成器和迭代器的區別
生成器的另一個優點就是它不要求你事先準備好整個迭代過程中所有的元素,即無須將物件的所有元素都存入記憶體之後,才開始進行操作。生成器僅在迭代至某個元素時才會將該元素放入記憶體,而在這之前或之後,元素可以不存在或者被銷燬。這個特點使得它特別適合用於遍歷一些巨大的或是無限的類序列物件,EG. 大檔案/大集合/大字典/斐波那契數列等。這個特點被稱為 延遲計算 或 惰性求值(Lazy evaluation),可以有效的節省記憶體。惰性求值實際上是現實了協同程式 的思想。
協同程式:是一個可以獨立執行的函式呼叫,該呼叫可以被暫停或者掛起,之後還能夠從程式流掛起的地方繼續或重新開始。當協同程式被掛起時,Python 就能夠從該協同程式中獲取一個處於中間狀態的屬性的返回值(由 yield 返回),當呼叫 next()
方法使得程式流回到協同程式中時,能夠為其傳入額外的或者是被改變了的引數,並且從上次掛起的下一條語句繼續執行。這是一種類似於程式中斷的函式呼叫方式。這種掛起函式呼叫並在返回屬性中間值後,仍然能夠多次繼續執行的協同程式被稱之為生成器。
NOTE:而迭代器是不具有上述的特性的,不適合去處理一些巨大的類序列物件,所以建議優先考慮使用生成器來處理迭代的場景。
生成器的優勢
綜上所述:使用生成器最好的場景就是當你需要以迭代的方式去穿越一個巨大的資料集合。比如:一個巨大的檔案/一個複雜的資料庫查詢等。
EXAMPLE 2:讀取一個大檔案
1 2 3 4 5 6 7 8 9 |
def read_file(fpath): BLOCK_SIZE = 1024 with open(fpath, 'rb') as f: while True: block = f.read(BLOCK_SIZE) if block: yield block else: return |
如果直接對檔案物件呼叫 read() 方法,會導致不可預測的記憶體佔用。好的方法是利用固定長度的緩衝區來不斷讀取檔案的部分內容。通過 yield,我們不再需要編寫讀檔案的迭代類,就可以輕鬆實現檔案讀取。
加強的生成器特性
除了可以使用 next()
方法來獲取下一個生成的值,使用者還可以使用 send()
方法將一個新的或者是被修改的值返回給生成器。除此之外,還可以使用 close()
方法來隨時退出生成器。
EXAMPLE 3:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
In [5]: def counter(start_at=0): ...: count = start_at ...: while True: ...: val = (yield count) ...: if val is not None: ...: count = val ...: else: ...: count += 1 ...: In [6]: count = counter(5) In [7]: type(count) Out[7]: generator In [8]: count.next() Out[8]: 5 In [9]: count.next() Out[9]: 6 In [10]: count.send(9) # 返回一個新的值給生成器中的 yield count Out[10]: 9 In [11]: count.next() Out[11]: 10 In [12]: count.close() # 關閉一個生成器 In [13]: count.next() --------------------------------------------------------------------------- StopIteration Traceback (most recent call last) <ipython-input-13-3963aa0a181a> in <module>() ----> 1 count.next() StopIteration: |
生成器表示式
生成器表示式是列表解析的擴充套件,就如上文所述:生成器是一個特定的函式,允許返回一箇中間值,然後掛起程式碼的執行,稍後再恢復執行。列表解析的不足在於,它必須一次性生成所有的資料,用以建立列表物件,所以不適用於迭代大量的資料。
生成器表示式通過結合列表解析和生成器來解決這個問題。
- 列表解析
[expr for iter_var in iterable if cond_expr]
- 生成器表示式
(expr for iter_var in iterable if cond_expr)
兩者的語法非常相似,但生成器表示式返回的不是一個列表型別物件,而是一個生成器物件,生成器是一個記憶體使用友好的結構。
生成器表示式樣例
通過改進查詢檔案中最長的行的功能實現來看看生成器的優勢。
EXAMPLE 4 : 一個比較通常的方法,通過迴圈將更長的行賦值給變數 longest 。
1 2 3 4 5 6 7 8 9 10 11 |
f = open('FILENAME', 'r') longest = 0 while True: linelen = len(f.readline().strip()) if not linelen: break if linelen > longest: longest = linelen f.close() return longest |
很明顯的,在這裡例子中,需要迭代的物件是一個檔案物件。
改進 1:
需要注意的是,如果我們讀取一個檔案所有的行,那麼我們應該儘早的去釋放這個檔案資源。例如:一個日誌檔案,會有很多不同的程式會其進行操作,所以我們不能容忍任意一個程式拿著這個檔案的控制程式碼不放。
1 2 3 4 5 6 7 8 9 10 11 12 |
f = open('FILENAME', 'r') longest = 0 allLines = f.readlines() f.close() for line in allLines: linelen = len(line.strip()) if not linelen: break if linelen > longest: longest = linelen return longest |
改進 2:
我們可以使用列表解析來簡化上述的程式碼,例如:在得到 allLines 所有行的列表時對每一行都進行處理。
1 2 3 4 5 6 7 8 9 10 11 12 |
f = open('FILENAME', 'r') longest = 0 allLines = [x.strip() for x in f.readlines()] f.close() for line in allLines: linelen = len(line) if not linelen: break if linelen > longest: longest = linelen return longest |
改進 3:
當我們處理一個巨大的檔案時,file.readlines()
並不是一個明智的選擇,因為 readlines()
會讀取檔案中所有的行。那麼我們是否有別的方法來獲取所有行的列表呢?我們可以應用 file 檔案內建的迭代器。
1 2 3 4 |
f = open('FILENAME', 'r') allLinesLen = [line(x.strip()) for x in f] f.close() return max(allLinesLen) # 返回列表中最大的數值 |
不再需要使用迴圈比較並保留當前最大值的方法來處理,將所有行的長度最後元素存放在列表物件中,再獲取做大的值即可。
改進 4:
這裡仍然存在一個問題,就是使用列表解析來處理 file 物件時,會將 file 所有的行都讀取到記憶體中,然後再建立一個新的列表物件,這是一個記憶體不友好的實現方式。那麼,我們就可以使用生成器表示式來替代列表解析。
1 2 3 4 |
f = open('FILENAME', 'r') allLinesLen = (line(x.strip()) for x in f) # 這裡的 x 相當於 yield x f.close() return max(allLinesLen) |
因為如果在函式中使用生成器表示式作為引數時,我們可以忽略括號 ‘()’,所以還能夠進一步簡化程式碼:
1 2 3 4 |
f = open('FILENAME', 'r') longest = max(line(x.strip()) for x in f) f.close() return longest |
最後:我們能夠以一行程式碼實現這個功能,讓 Python 解析器去處理開啟的檔案。
當然並不是說程式碼越少就越好,例如下面這一行程式碼每迴圈一次就會呼叫一個 open()
函式,效率上並沒有 改進 4 更高。
1 |
return max(line(x.strip()) for x in open('FILENAME')) |
小結
在需要迭代穿越一個物件時,我們應該優先考慮使用生成器替代迭代器,使用生成器表示式替代列表解析。當然這並不是絕對的。 迭代器和生成器是 Python 很重要的特性,對其有很好的理解能夠寫出更加 Pythonic 的程式碼。