Python 進階_迭代器 & 列表解析python
帶有 yield 關鍵字的的函數在 Python 中被稱之爲 generator(生成器)。Python 解釋器會將帶有 yield 關鍵字的函數視爲一個 generator 來處理。一個函數或者子程序都只能 return 一次,可是一個生成器能暫停執行並返回一箇中間的結果 —— 這就是 yield 語句的功能 : 返回一箇中間值給調用者並暫停執行。數據庫
EXAMPLE:函數
Pythonlua
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()
的執行過程spa
執行語句 f = fab(5)
時,並不會立刻執行 fab()
函數的代碼塊,而是首先返回一個 iterable 對象!
在 for 循環語句執行時,纔會執行 fab()
函數的代碼塊。
執行到語句 yield b
時,fab()
函數會返回一個迭代值,直到下次迭代前,程序流會回到 yield b
的下一條語句繼續執行,而後再次回到 for 循環,如此迭代直到結束。看起來就好像一個函數在正常執行的過程當中被 yield
中斷了數次,每次中斷都會經過 yield
返回當前的迭代值。
由此能夠看出,生成器經過關鍵字 yield
不斷的將迭代器返回到內存進行處理,而不會一次性的將對象所有放入內存,從而節省內存空間。從這點看來生成器和迭代器很是類似,但若是更深刻的瞭解的話,其實二者仍存在區別。.net
生成器的另外一個優勢就是它不要求你事先準備好整個迭代過程當中全部的元素,即無須將對象的全部元素都存入內存以後,纔開始進行操做。生成器僅在迭代至某個元素時纔會將該元素放入內存,而在這以前或以後,元素能夠不存在或者被銷燬。這個特色使得它特別適合用於遍歷一些巨大的或是無限的類序列對象,EG. 大文件/大集合/大字典/斐波那契數列等。這個特色被稱爲 延遲計算 或 惰性求值(Lazy evaluation),能夠有效的節省內存。惰性求值其實是現實了協同程序 的思想。日誌
協同程序:是一個能夠獨立運行的函數調用,該調用能夠被暫停或者掛起,以後還可以從程序流掛起的地方繼續或從新開始。當協同程序被掛起時,Python 就可以從該協同程序中獲取一個處於中間狀態的屬性的返回值(由 yield 返回),當調用 next()
方法使得程序流回到協同程序中時,可以爲其傳入額外的或者是被改變了的參數,而且從上次掛起的下一條語句繼續執行。這是一種相似於進程中斷的函數調用方式。這種掛起函數調用並在返回屬性中間值後,仍然可以屢次繼續執行的協同程序被稱之爲生成器。code
NOTE:而迭代器是不具備上述的特性的,不適合去處理一些巨大的類序列對象,因此建議優先考慮使用生成器來處理迭代的場景。對象
綜上所述:使用生成器最好的場景就是當你須要以迭代的方式去穿越一個巨大的數據集合。好比:一個巨大的文件/一個複雜的數據庫查詢等。blog
EXAMPLE 2:讀取一個大文件
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:
Python
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 。
Python
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:很明顯的,在這裏例子中,須要迭代的對象是一個文件對象。
須要注意的是,若是咱們讀取一個文件全部的行,那麼咱們應該儘早的去釋放這個文件資源。例如:一個日誌文件,會有不少不一樣的進程會其進行操做,因此咱們不能容忍任意一個進程拿着這個文件的句柄不放。
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 全部行的列表時對每一行都進行處理。
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 文件內置的迭代器。
f = open('FILENAME', 'r') allLinesLen = [line(x.strip()) for x in f] f.close() return max(allLinesLen) # 返回列表中最大的數值
改進 4:再也不須要使用循環比較並保留當前最大值的方法來處理,將全部行的長度最後元素存放在列表對象中,再獲取作大的值便可。
這裏仍然存在一個問題,就是使用列表解析來處理 file 對象時,會將 file 全部的行都讀取到內存中,而後再建立一個新的列表對象,這是一個內存不友好的實現方式。那麼,咱們就可使用生成器表達式來替代列表解析。
f = open('FILENAME', 'r') allLinesLen = (line(x.strip()) for x in f) # 這裏的 x 至關於 yield x f.close() return max(allLinesLen)
由於若是在函數中使用生成器表達式做爲參數時,咱們能夠忽略括號 ‘()’,因此還可以進一步簡化代碼:
f = open('FILENAME', 'r') longest = max(line(x.strip()) for x in f) f.close() return longest
最後:咱們可以以一行代碼實現這個功能,讓 Python 解析器去處理打開的文件。
固然並非說代碼越少就越好,例以下面這一行代碼每循環一次就會調用一個 open()
函數,效率上並無 改進 4 更高。
return max(line(x.strip()) for x in open('FILENAME'))
小結
在須要迭代穿越一個對象時,咱們應該優先考慮使用生成器替代迭代器,使用生成器表達式替代列表解析。固然這並非絕對的。 迭代器和生成器是 Python 很重要的特性,對其有很好的理解可以寫出更加 Pythonic 的代碼。