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


解析、迭代和生成系列文章:https://www.cnblogs.com/f-ck-need-u/p/9832640.html


何為生成器

生成器的wiki頁:https://en.wikipedia.org/wiki/Generator_(computer_programming)

在計算機科學中,生成器是特定的迭代器,它完全實現了迭代器接口,所以所有生成器都是迭代器。不過,迭代器用於從數據集中取出元素;而生成器用於"憑空"生成(yield)元素。它不會一次性將所有元素全部生成,而是按需一個一個地生成,所以從頭到尾都只需占用一個元素的內存空間。

很典型的一個例子是斐波納契數列:斐波納契數列中的數有無窮個,在一個數據結構里放不下,但是可以在需要下一個元素的時候臨時計算。

再比如內置函數range()也返回一個類似生成器的對象,每次需要range里的一個數據時才會臨時去產生它。如果一定要讓range()函數返回列表,必須明確指明list(range(100))

在Python中生成器是一個函數,但它的行為像是一個迭代器。另外,Python也支持生成器表達式。

初探生成器

下面是一個非常簡單的生成器示例:

>>> 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語句一樣。

只不過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) )


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM