PythonI/O進階學習筆記_9.python的生成器
阿新 • • 發佈:2020-01-06
content:
1. 什麼是生成器
2. 生成器的實現
3. 生成器的應用
一.生成器簡介
1.什麼是生成器
在 Python 中,使用了 yield 的函式被稱為生成器(generator)。
跟普通函式不同的是,生成器是一個返回迭代器的函式,只能用於迭代操作,更簡單點理解生成器就是一個迭代器。
在呼叫生成器執行的過程中,每次遇到 yield 時函式會暫停並儲存當前所有的執行資訊,返回 yield 的值, 並在下一次執行 next() 方法時從當前位置繼續執行。
呼叫一個生成器函式,返回的是一個迭代器物件。
可以看到,普通函式就是返回的return,而生成器函式是生成了一個生成器物件。
為什麼能和普通函式不一樣返回生成器物件?
因為在python在執行之前進行編譯成位元組碼。發現了yield關鍵字,所以在編譯的時候就定義了。
生成器物件,實際上也是實現了我們的迭代協議的。
為啥會用到生成器?
簡單舉個例子:
列表所有資料都在記憶體中,如果有海量資料的話將會非常耗記憶體。如果僅僅需要訪問前面幾個元素,那後面絕大多數元素佔用的空間都白白浪費了。
如果列表元素能按照某種演算法推算出來,那我們就可以在迴圈的過程中不斷推算出後續的元素,這樣就不必建立完整的list,從而節省大量的空間。
生成器在python中的設計使用
實現了延遲求值和惰性求值,也是後面協程實現的基礎。
2.生成器怎麼用
例子:實現斐波拉契數列
在呼叫函式之前,會建立那個棧幀物件,然後在上下文中,執行這個全域性唯一的位元組碼。
當foo呼叫bar的時候,又會建立一個棧幀,然後將bar的控制權交給foo的棧幀物件。
所有棧幀都是分配在堆記憶體(不去釋放,就一直在記憶體中)上,這就決定了棧幀可以獨立於呼叫者存在。
什麼意思呢?
就是在foo函式退出之後,我們仍然可以找到之前呼叫過的foo,或者它的子函式bar的棧幀,並沒有和靜態語言一樣函式執行完了之後就被釋放。
2.生成器物件原理
假設我們實現一個生成器函式:
這個生成器物件實際上如下圖所示:
實際上,就是在1中的PyFrameObject上面,再封了一層,為PyGenObject。
並且,再yield時候,實際上就是暫停了最近的那句程式碼。當時的上下文都是被儲存的,即f_lasti,f_locals。在任何地方都可以暫停和控制它。
檢視yield時,儲存的lasti和locals:
#input def fib(x): if x<3: return 1 else: return fib(x-1)+fib(x-2) def fib2(x): n=0 last=1 sum=0 while n<x: yiled last sum,last=last,sum+last n=n+1 if __name__=="__main__": f=fib(6) print(f) f2=fib2(6) for i in f2: print(i) pass #output 8 1 1 2 3 5 8
二. 生成器的實現 生成器其實用起來還是比較簡單的,但是不理解原理的時候,用的時候是不是虛虛的。 1.python函式的工作原理 python直譯器實際上是用c來寫的。直譯器會用C實現的函式(PyEval_EvalFramEx)去執行函式。 這個 PyEval_EvalFramEx 首先會建立一個棧幀(Stack Frame)物件,就是那種記錄上下文的堆疊。注意python裡一切皆物件哦。 然後會將程式碼也變成位元組碼物件。檢視一個函式的位元組碼:
#input def foo(): bar def bar(): pass import dis print(dis.dis(foo)) #output: 2 0 LOAD_GLOBAL 0 (bar) 2 POP_TOP 4 LOAD_CONST 0 (None) 6 RETURN_VALUE None
def gen_func(): yield 1 name="bobby" yield 2 age=30 return "tangrong"
#input def gen_func(): yield 1 name="bobby" yield 2 age=30 return "tangrong" gen=gen_func() print(gen.gi_frame.f_lasti) print(gen.gi_frame.f_locals) next(gen) print(gen.gi_frame.f_lasti) print(gen.gi_frame.f_locals) next(gen) print(gen.gi_frame.f_lasti) print(gen.gi_frame.f_locals) #outpu: -1 {} 2 {} 12 {'name': 'bobby'}
三.生成器的應用 1.生成器在Userlist中的應用 我們知道,對list可以進行迴圈遍歷。因為其是可迭代的。如果實現了__getitem__也是可以進行for遍歷的。而且是會先去查詢__iter__,沒有發現__iter__魔法方法才會去找__getitem__方法。 我們去看list類的原始碼的時候,其實就是提供了給我們看的介面,實際的c語言實現並看不到。而且我們在定製自己的List類的時候,是完全不提倡去繼承list的,因為裡面的很多關鍵方法是不能被重寫的。但是python提供了UserList,即python實現的list。 首先,UserList是繼承的MutableSequence。 而在MutableSequence的__iter__的實現中,就應用到了生成器。 2.生成器是如何讀取大檔案的(如何使用生成器表示式) #將一個500g的檔案讀取出來 寫入資料庫 並且這個檔案只有一行資料,有特定的分隔符。 #如果是多行的話,用open一行行讀取還是可以的。 #但是實際上!! 檔案物件的read函式,是可以傳遞我要讀取的大小的,並且偏移量會被記錄。