Python迭代和解析(5):搞懂生成器和yield機制

解析、迭代和生成系列文章: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 from

當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如何工做

理解這個工做過程很是重要,是理解和掌握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的返回值和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()的過程。

解釋yield的第一個示例

這個示例比較簡單。

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的第二個示例

這個示例稍微複雜些,但理解了前面的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()函數的用法:

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) )
相關文章
相關標籤/搜索