給妹子講python--16生成器的使用
陪伴學習,一路成長。請一起關注,一起點贊吧!
【要點搶先看】
1.生成器函數的使用n2.生成器表達式的使用n3.與列表解析式的對比及對內存的優化n
之前我們介紹了列表解析式,他的優點很多,比如運行速度快、編寫簡單,但是有一點我們不要忘了,他是一次性生成整個列表。如果整個列表非常大,這對內存也同樣會造成很大壓力,想要實現內存的節約,可以將列表解析式轉換為生成器表達式。
【妹子說】那今天就要說說生成器咯。
對的,避免一次性生成整個結果列表的本質是在需要的時候才逐次產生結果,而不是立即產生全部的結果,python中有兩種語言結構可以實現這種思路。
一個是生成器函數。外表看上去像是一個函數,但是沒有用return語句一次性的返回整個結果對象列表,取而代之的是使用yield語句一次返回一個結果。另一個是生成器表達式。類似於上一小節的列表解析,但是方括弧換成了圓括弧,他們返回按需產生的一個結果對象,而不是構建一個結果列表。
這個「按需」指的是在迭代的環境中,每次迭代按需產生一個對象,因此,上述二者都不會一次性構建整個列表,從而節約了內存空間。
【妹子說】那舉幾個例子說說吧。
好,下面具體結合例子說說生成器函數。
首先,我們還沒有詳細介紹過函數,先簡單說一下,常規函數接受輸入的參數然後立即送回單個結果,之後這個函數調用就結束了。
但生成器函數卻不同,他通過yield關鍵字返回一個值後,還能從其退出的地方繼續運行,因此可以隨時間產生一系列的值。他們自動實現了迭代協議,並且可以出現在迭代環境中。
運行的過程是這樣的:生成器函數返回一個迭代器,for循環等迭代環境對這個迭代器不斷調用next函數,不斷的運行到下一個yield語句,逐一取得每一個返回值,直到沒有yield語句可以運行,最終引發StopIteration異常。看,這個過程是不是很熟悉。
首先,下面這個例子證實了生成器函數返回的是一個迭代器
def gen_squares(num):nfor x in range(num):nyield x ** 2nG = gen_squares(5)nprint(G)nprint(iter(G))nn<generator object gen_squares at 0x0000000002402558>n<generator object gen_squares at 0x0000000002402558>n
然後再用手動模擬循環的方式來看看生成器函數的運行過程,你會發現和前面介紹過的熟悉場景並無二致。
def gen_squares(num):nfor x in range(num):nyield x ** 2nG = gen_squares(3)nprint(G)nprint(iter(G))nprint(next(G))nprint(next(G))nprint(next(G))nprint(next(G))nnn<generator object gen_squares at 0x00000000021C2558>n<generator object gen_squares at 0x00000000021C2558>n0n1n4nTraceback (most recent call last):n File "E:/12homework/12homework.py", line 10, in <module>nprint(next(G))nStopIterationn
那這麼看,在for循環等真正的使用場景中使用也不難了
def gen_squares(num):nfor x in range(num):nyield x ** 2nnfor i in gen_squares(5):nprint(i, end= )nn0 1 4 9 16 n
我們進一步來說說生成器函數里狀態保存的話題。在每次循環的時候,生成器函數都會在yield處產生一個值,並將其返回給調用者,即for循環。然後在yield處保存內部狀態,並掛起中斷退出。在下一輪迭代調用時,從yield的地方繼續執行,並且沿用上一輪的函數內部變數的狀態,直到內部循環過程結束。
關於這個問題,具體可以看看這個例子:
def gen_squares(num):nfor x in range(num):nyield x ** 2nprint(x={}.format(x))nnfor i in gen_squares(4):nprint(x ** 2={}.format(i))nprint(--------------)nnx ** 2=0n--------------nx=0nx ** 2=1n--------------nx=1nx ** 2=4n--------------nx=2nx ** 2=9n--------------nx=3n
我們不難發現,生成器函數計算出x的平方後就掛起退出了,但他仍然保存了此時x的值,而yield後的print語句會在for循環的下一輪迭代中首先調用,此時x的值即是上一輪退出時保存的值。
【妹子說】那再說說生成器表達式吧。
列表解析式已經是一個不錯的選擇,從內存使用的角度而言,生成器更優,因為他不用一次性生成整個對象列表,這二者之間如何轉化呢?
生成器表達式寫法上很像列表解析式,但是外面的方括弧換成了圓括弧,結果大不同
簡單的看看:
print([x ** 2 for x in range(5)])nprint((x ** 2 for x in range(5)))nn[0, 1, 4, 9, 16]n<generator object <genexpr> at 0x0000000002212558>n
方括弧是熟悉的列表解析式,一次性返回整個列表,圓括弧是生成器表達式,返回一個生成器對象,而不是一次性生成整個列表。
同時他支持迭代協議,適用於所有的迭代環境:
略舉幾個例子:
for x in (x ** 2 for x in range(5)):nprint(x, end=,)nn0,1,4,9,16,nnnprint(sum(x ** 2 for x in range(5)))n30nnnprint(sorted((x ** 2 for x in range(5)), reverse=True))n[16, 9, 4, 1, 0]nnnprint(list(x ** 2 for x in range(5)))n[0, 1, 4, 9, 16]n
總結:生成器表達式是對內存空間的優化。他們不需要像方括弧的列表解析一樣,一次構造出整個結果列表。他們運行起來比列表解析式可能稍慢一些,因此他們對於非常大的結果集合運算是最優的選擇。
【妹子說】那總結起來一句話:列表解析式最快,生成器表達式最省空間,速度也還可以。
補充說明一下:
集合解析式等效於將生成器對象傳入到list、set、dict等函數中作為構造參數
set(f(x) for x in S if P(x))n{f(x) for x in S if P(x)}nn{key:val for (key, val) in zip(keys, vals)}ndict(zip(keys, vals))nn{x:f(x) for x in items}ndict((x, f(x)) for x in items)n
推薦閱讀:
※AP演算法中兩個參數的交替過程怎麼樣通俗的理解?
※「遞歸」和「迭代」有哪些區別?
※c++ 二叉樹的中序遍歷(非遞歸實現)是哪裡出錯了?
※客戶端產品迭代周期為多長時間比較合適?