給妹子講python-S01E17談談函式的基本特徵

醬油哥在掘金發表於2018-08-07

歡迎關注公眾號:python資料科學家

【要點搶先看】

1.生成器函式的使用
2.生成器表示式的使用
3.與列表解析式的對比及對記憶體的優化

之前我們介紹了列表解析式,他的優點很多,比如執行速度快、編寫簡單,但是有一點我們不要忘了,他是一次性生成整個列表。如果整個列表非常大,這對記憶體也同樣會造成很大壓力,想要實現記憶體的節約,可以將列表解析式轉換為生成器表示式。

【妹子說】那今天就要說說生成器咯。

對的,避免一次性生成整個結果列表的本質是在需要的時候才逐次產生結果,而不是立即產生全部的結果,python中有兩種語言結構可以實現這種思路。

一個是生成器函式。外表看上去像是一個函式,但是沒有用return語句一次性的返回整個結果物件列表,取而代之的是使用yield語句一次返回一個結果。另一個是生成器表示式。類似於上一小節的列表解析,但是方括號換成了圓括號,他們返回按需產生的一個結果物件,而不是構建一個結果列表。

這個“按需”指的是在迭代的環境中,每次迭代按需產生一個物件,因此,上述二者都不會一次性構建整個列表,從而節約了記憶體空間。

【妹子說】那舉幾個例子說說吧。

好,下面具體結合例子說說生成器函式。

首先,我們還沒有詳細介紹過函式,先簡單說一下,常規函式接受輸入的引數然後立即送回單個結果,之後這個函式呼叫就結束了。

但生成器函式卻不同,他通過yield關鍵字返回一個值後,還能從其退出的地方繼續執行,因此可以隨時間產生一系列的值。他們自動實現了迭代協議,並且可以出現在迭代環境中。

執行的過程是這樣的:生成器函式返回一個迭代器,for迴圈等迭代環境對這個迭代器不斷呼叫next函式,不斷的執行到下一個yield語句,逐一取得每一個返回值,直到沒有yield語句可以執行,最終引發StopIteration異常。看,這個過程是不是很熟悉。

首先,下面這個例子證實了生成器函式返回的是一個迭代器

def gen_squares(num):
    for x in range(num):
        yield x ** 2

G = gen_squares(5)
print(G)
print(iter(G))

<generator object gen_squares at 0x0000000002402558>
<generator object gen_squares at 0x0000000002402558>
複製程式碼

然後再用手動模擬迴圈的方式來看看生成器函式的執行過程,你會發現和前面介紹過的熟悉場景並無二致。

def gen_squares(num):
    for x in range(num):
        yield x ** 2

G = gen_squares(3)
print(G)
print(iter(G))
print(next(G))
print(next(G))
print(next(G))
print(next(G))


<generator object gen_squares at 0x00000000021C2558>
<generator object gen_squares at 0x00000000021C2558>
0
1
4
Traceback (most recent call last):
 File "E:/12homework/12homework.py", line 10in <module>
print(next(G))
StopIteration
複製程式碼

那這麼看,在for迴圈等真正的使用場景中使用也不難了

def gen_squares(num):
    for x in range(num):
        yield x ** 2

for i in gen_squares(5):
    print(i, end=' ')

0 1 4 9 16 
複製程式碼

我們進一步來說說生成器函式裡狀態儲存的話題。在每次迴圈的時候,生成器函式都會在yield處產生一個值,並將其返回給呼叫者,即for迴圈。然後在yield處儲存內部狀態,並掛起中斷退出。在下一輪迭代呼叫時,從yield的地方繼續執行,並且沿用上一輪的函式內部變數的狀態,直到內部迴圈過程結束。

關於這個問題,具體可以看看這個例子:

def gen_squares(num):
    for x in range(num):
        yield x ** 2
        print('x={}'.format(x))

for i in gen_squares(4):
    print('x ** 2={}'.format(i))
    print('--------------')

x ** 2=0
--------------
x=0
x ** 2=1
--------------
x=1
x ** 2=4
--------------
x=2
x ** 2=9
--------------
x=3
複製程式碼

我們不難發現,生成器函式計算出x的平方後就掛起退出了,但他仍然儲存了此時x的值,而yield後的print語句會在for迴圈的下一輪迭代中首先呼叫,此時x的值即是上一輪退出時儲存的值。

【妹子說】那再說說生成器表示式吧。

列表解析式已經是一個不錯的選擇,從記憶體使用的角度而言,生成器更優,因為他不用一次性生成整個物件列表,這二者之間如何轉化呢?

生成器表示式寫法上很像列表解析式,但是外面的方括號換成了圓括號,結果大不同

簡單的看看:

print([x ** 2 for x in range(5)])
print((x ** 2 for x in range(5)))

[0, 1, 4, 9, 16]
<generator object <genexpr> at 0x0000000002212558>
複製程式碼

方括號是熟悉的列表解析式,一次性返回整個列表,圓括號是生成器表示式,返回一個生成器物件,而不是一次性生成整個列表。

同時他支援迭代協議,適用於所有的迭代環境:

略舉幾個例子:

for x in (x ** 2 for x in range(5)):
    print(x, end=',')

0,1,4,9,16,
複製程式碼

-

print(sum(x ** 2 for x in range(5)))
30
複製程式碼

-

print(sorted((x ** 2 for x in range(5)), reverse=True))
[16, 9, 4, 1, 0]
複製程式碼

-

print(list(x ** 2 for x in range(5)))
[0, 1, 4, 9, 16]
複製程式碼

總結:生成器表示式是對記憶體空間的優化。他們不需要像方括號的列表解析一樣,一次構造出整個結果列表。他們執行起來比列表解析式可能稍慢一些,因此他們對於非常大的結果集合運算是最優的選擇。

【妹子說】那總結起來一句話:列表解析式最快,生成器表示式最省空間,速度也還可以。

補充說明一下:

集合解析式等效於將生成器物件傳入到list、set、dict等函式中作為構造引數

set(f(x) for x in S if P(x))
{f(x) for x in S if P(x)}

{key:val for (key, val) in zip(keys, vals)}
dict(zip(keys, vals))

{x:f(x) for x in items}
dict((x, f(x)) for x in items)
複製程式碼

公眾號二維碼:python資料科學家:

給妹子講python-S01E17談談函式的基本特徵

相關文章