解析、迭代和生成系列文章:http://www.javashuo.com/article/p-aspbesnv-du.htmlhtml
生成器的wiki頁:https://en.wikipedia.org/wiki/Generator_(computer_programming)python
在計算機科學中,生成器是特定的迭代器,它徹底實現了迭代器接口,因此全部生成器都是迭代器。不過,迭代器用於從數據集中取出元素;而生成器用於"憑空"生成(yield)元素。它不會一次性將全部元素所有生成,而是按需一個一個地生成,因此從頭至尾都只需佔用一個元素的內存空間。git
很典型的一個例子是斐波納契數列:斐波納契數列中的數有無窮個,在一個數據結構裏放不下,可是能夠在須要下一個元素的時候臨時計算。數據結構
再好比內置函數range()也返回一個相似生成器的對象,每次須要range裏的一個數據時纔會臨時去產生它。若是必定要讓range()函數返回列表,必須明確指明list(range(100))
。併發
在Python中生成器是一個函數,但它的行爲像是一個迭代器。另外,Python也支持生成器表達式。app
下面是一個很是簡單的生成器示例:函數
>>> def my_generator(chars): ... for i in chars: ... yield i * 2 >>> for i in my_generator("abcdef"): ... print(i, end=" ") aa bb cc dd ee ff
這裏的my_generator
是生成器函數(使用了yield關鍵字的函數,將被聲明爲generator對象),可是它在for循環中充當的是一個可迭代對象。實際上它自己就是一個可迭代對象:優化
>>> E = my_generator("abcde") >>> hasattr(E, "__iter__") True >>> hasattr(E, "__next__") True >>> E is iter(E) True
因爲生成器自動實現了__iter__
和__next__
,且__iter__
返回的是迭代器自身,因此生成器是一個單迭代器,不支持多迭代。編碼
此外,生成器函數中使用for來迭代chars變量,但對於chars中被迭代的元素沒有其它操做,而是使用yield來返回這個元素,就像return語句同樣。spa
只不過yield和return是有區別的,yield在生成一個元素後,會記住迭代的位置並將當前的狀態掛起(還記住了其它一些必要的東西),等到下一次須要元素的時候再從這裏繼續yield一個元素,直到全部的元素都被yield完(也可能永遠yield不完)。return則是直接退出函數,
當yield的來源爲一個for循環,那麼能夠改寫成yield from。也就是說,for i in g:yield i
等價於yield from g
。
例以下面是等價的。
def mygen(chars): yield from chars def mygen(chars): for i in chars: yiled i
yield from更多地用於子生成器的委託,本文暫不對此展開描述。
下面是直接構造出列表的方式,它和前面示例的生成器結果同樣,可是內部工做方式是不同的。
def mydef(chars): res = [] for i in chars: res.append(i * 2) return res for i in mydef("abcde"): print(i,end=" ")
這樣的結果也能使用列表解析或者map來實現,例如:
for x in [s * 2 for s in "abcde"]: print(x, end=" ") for x in map( (lambda s: s * 2), "abcde" ): print(x, end=" ")
雖然結果上都相同,可是內存使用上和效率上都有區別。直接構造結果集將會等待全部結果都計算完成後一次性返回,可能會佔用大量內存並出現結果集等待的現象。而使用生成器的方式,從頭至尾都只佔用一個元素的內存空間,且無需等待全部元素都計算完成後再返回,因此將時間資源分佈到了每一個結果的返回上。
例如總共可能會產生10億個元素,但只想取前10個元素,若是直接構造結果集將佔用巨量內存且等待很長時間,但使用生成器的方式,這10個元素根本不需等待,很快就計算出來。
理解這個工做過程很是重要,是理解和掌握yield的關鍵。
1.調用生成器函數的時候並無運行函數體中的代碼,它僅僅只是返回一個生成器對象。
正以下面的示例,並不是輸出任何內容,說明沒有執行生成器函數體。
def my_generator(chars): print("before") for i in chars: yield i print("after") >>> c = my_generator("abcd") >>> c <generator object my_generator at 0x000001DC167392A0> >>> I = iter(c)
2.只有開始迭代的時候,才真正開始執行函數體。且在yield以前的代碼體只執行一次,在yield以後的代碼體只在當前yield結束的時候才執行。
>>> next(I) before # 第一次迭代 'a' >>> next(I) 'b' >>> next(I) 'c' >>> next(I) 'd' >>> next(I) after # 最後一次迭代,拋出異常中止迭代 Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
一個生成器函數能夠有多個yield語句,看看下面的執行過程:
def mygen(): print("1st") yield 1 print("2nd") yield 2 print("3rd") yield 3 print("end") >>> m = mygen() >>> next(m) 1st 1 >>> next(m) 2nd 2 >>> next(m) 3rd 3 >>> next(m) end Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
到此,想必已經理解了yield的工做過程。但還有一些細節有必要解釋清楚。
yield是一個表達式,但它是有返回值的。須要注意的是,yield操做會在產生併發送了值以後當即讓函數處於掛起狀態,掛起的時候連返回值都還沒來得及返回。因此,yield表達式的返回值是在下一次迭代時才生成返回值的。關於yield的返回值相關,見下面的生成器的send()方法。
上面說了,yield有返回值,且其返回值是在下一次迭代的時候才返回的。它的返回值根據恢復yield的方式不一樣而不一樣。
yield有如下幾種常見的表達式組合方式:
yield 10 # (1) 丟棄yield的返回值 x = yield 10 # (2) 將yield返回值賦值給x x = (yield 10) # (3) 等價於 (2) x = (yield 10) + 11 # (4) 將yield返回值加上11後賦值給x
無論yield表達式的編碼方式如何,它的返回值都和調用next()(或__next__()
)仍是生成器對象的send()方法有關。這裏的send()方法和next()都用於恢復當前掛起的yield。
若是是調用next()來恢復yield,那麼yield的返回值爲None,若是調用gen.send(XXX)
來恢復yield,那麼yield的返回值爲XXX
。其實next()能夠看做是等價於gen.send(None)
。
再次提醒,yield表達式會在產生一個值後當即掛起,它連返回值都是在下一次才返回的,更不用說yield的賦值和yield的加法操做。
因此,上面的4種yield表達式方式中,若是使用next()來恢復yield,則它們的值分別爲:
yield 10 # 先產生10發送出去,而後返回None,但丟棄 x = yield 10 # 返回None,賦值給x x = (yield 10) # 與上等價 x = (yield 10)+11 # 返回None,整個過程報錯,由於None和int不能相加
若是使用的是send(100),上面的4種yield表達式方式中的值分別爲:
yield 10 # 先產生10發送出去,而後返回100,但丟棄 x = yield 10 # 返回100,賦值給x,x=100 x = (yield 10) # 與上等價 x = (yield 10)+11 # 返回100,加上11後賦值給x,x=111
爲了解釋清楚yield工做時的返回值問題,我將用兩個示例詳細地解釋每一次next()/send()的過程。
這個示例比較簡單。
def mygen(): x = yield 111 # (1) print("x:", x) # (2) for i in range(5): # (3) y = yield i # (4) print("y:", y) # (5) M = mygen()
1.首先執行下面的代碼
>>> print("first:",next(M)) 111
這一行執行後,首先將yield出來的111傳遞給調用者,而後當即在(1)處進行掛起,這時yield表達式尚未進入返回值狀態,因此x還未進行賦值操做。可是next(M)
已經返回了,因此print正常輸出。
不管是next()(或__next__
)仍是send()均可以用來恢復掛起的yield,但第一次進入yield必須使用next()或者使用send(None)來產生一個掛起的yield。假如第一次就使用send(100)
,因爲此時尚未掛起的yield,因此沒有yield須要返回值,這會報錯。
2.再執行下面的代碼
>>> print("second:",M.send(10)) x: 10 second: 0
這裏的M.send(10)
首先恢復(1)處掛起的yield,並將10做爲該yield的返回值,因此x = 10
,而後生成器函數的代碼體繼續向下執行,到了print("x:",x)
正常輸出。
再繼續進入到for循環迭代中,又再次遇到了yield,因而yield產生range(5)的第一個數值0傳遞給調用者而後當即掛起,因而M.send()
等待到了這個yield值,因而輸出"second: 0"。但注意,這時候y尚未進行賦值,由於yield尚未進入返回值的過程。
3.再執行下面的代碼
>>> print("third:",M.send(11)) y: 11 third: 1
這裏的M.send(11)
首先恢復上次掛起的yield並將11做爲該掛起yield的返回值,因此y=11,由於yield已經恢復,因此代碼體繼續詳細執行print("y:",y)
,執行以後進入下一輪for迭代,因而再次遇到yield,它生成第二個range的值1並傳遞給調用者,而後掛起,因而M.send()
接收到數值1並返回,因而輸出third: 1
。注意,此時的y仍然是11,由於for的第二輪yield尚未返回。
4.繼續執行,但使用next()
>>> print("fourth:",next(M)) y: None fourth: 2
這裏的next(M)恢復前面掛起的yield,而且將None做爲yield的返回值,因此y賦值爲None。而後進入下一輪for循環、遇到yield,next()接收yield出來的值2並返回。
next()能夠看做等價於M.send(None)
。
5.依此類推,直到迭代結束拋出異常
>>> print("fifth:",M.send(13)) y: 13 fifth: 3 >>> print("sixth:",M.send(14)) y: 14 sixth: 4 >>> print("seventh:",M.send(15)) # 看此行 y: 15 Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
當發送M.send(15)
時,前面掛起的yield恢復並以15做爲返回值,因此y=15
。因而繼續執行,但此時for迭代已經完成了,因而拋出異常,整個生成器函數終止。
這個示例稍微複雜些,但理解了前面的yield示例,這個示例也很容易理解。注意,下面的代碼不要在交互式python環境中執行,而是以py腳本的方式執行。
def gen(): for i in range(5): X = int((yield i) or 0) + 10 + i print("X:",X) G = gen() for a in G: print(a) G.send(77)
執行結果爲:
0 X: 87 X: 11 2 X: 89 X: 13 4 X: 91 Traceback (most recent call last): File "g:\pycode\lists.py", line 10, in <module> G.send(77) StopIteration
這裏for a in G
用的是next(),在這個for循環裏又用了G.send()
,由於send()接收的值在空上下文,因此被丟棄,但它卻將生成器向前移動了一步。
更多的細節請自行思考,如不理解可參考上一個示例的分析。
列表解析/字典解析/集合解析是使用中括號、大括號包圍for表達式的,而生成器表達式則是使用小括號包圍for表達式,它們的for表達式寫法徹底同樣。
# 列表解析 >>> [ x * 2 for x in range(5) ] [0, 2, 4, 6, 8] # 生成器表達式 >>> ( x * 2 for x in range(5) ) <generator object <genexpr> at 0x0000013F550A92A0>
在結果上,列表解析等價於list()函數內放生成器表達式:
>>> [ x * 2 for x in range(5) ] [0, 2, 4, 6, 8] >>> list( x * 2 for x in range(5) ) [0, 2, 4, 6, 8]
可是工做方式徹底不同。列表解析等待全部元素都計算完成後一次性返回,而生成器表達式則是返回一個生成器對象,而後一個一個地生成並構建成列表。生成器表達式能夠看做是列表解析的內存優化操做,但執行速度上可能要稍慢於列表解析。因此生成器表達式和列表解析之間,在結果集很是大的時候能夠考慮採用生成器表達式。
通常來講,若是生成器表達式做爲函數的參數,只要該函數沒有其它參數均可以省略生成器表達式的括號,若是有其它參數,則須要括號包圍避免歧義。例如:
sum( x ** 2 for x in range(4)) sorted( x ** 2 for x in range(4)) sorted((x ** 2 for x in range(4)),reverse=True)
生成器表達式通常用來寫較爲簡單的生成器對象,生成器函數代碼可能稍多一點,但能夠實現邏輯更爲複雜的生成器對象。它們的關係就像列表解析和普通的for循環同樣。
例如,將字母重複4次的生成器對象,能夠寫成下面兩種格式:
# 生成器表達式 t1 = ( x * 4 for x in "hello" ) # 生成器函數 def time4(chars): for x in chars: yield x * 4 t2 = time4("abcd")
map()函數的用法:
map(func, *iterables) --> map object
要想模擬map函數,先看看map()對應的for模擬方式:
def mymap(func,*seqs): res = [] for args in zip(*args): res.append( func(*args) ) return res print( mymap(pow, [1,2,3], [2,3,4,5]) )
對此,能夠編寫出更精簡的列表解析方式的map()模擬代碼:
def mymap(func, *seqs): return [ func(*args) for args in zip(*seqs) ] print( mymap(pow, [1,2,3], [2,3,4,5]) )
若是要用生成器來模擬這個map函數,能夠參考以下代碼:
# 生成器函數方式 def mymap(func, *seqs): res = [] for args in zip(*args): yield func(*args) # 或者生成器表達式方式 def mymap(func, *seqs): return ( func(*args) for args in zip(*seqs) )