Python 發生器
Python 發生器是一種惰性迭代器,它通過發生器函式(使用 yield
關鍵字)來建立,或者通過發生器表示式(比如 an_expression for x in an_iterator
)。
介紹
生成器表示式類似於列表、字典或者集合的推導式,但是用括號括起來。但當它們用作函式呼叫的唯一引數時,不必非得加括號,比如
expressionEg = (x ** 2 for x in range(10))
此例生成從 0 開始的前 10 個數字的平方。
生成器函式類似於常規函式,除了它們在主體中有一個或多個 yield
語句。這些函式不能返回(return
)任何值(但如果你想提前停止生成器,則允許使用空 return
)。
def function():
for x in range(10):
yield x ** 2
此生成器函式跟前一個生成器表示式等效,它們輸出相同的結果。
生成器表示式是函式括號內的唯一引數,則可以使用不帶括號的生成器表示式,否則的話,就會出現兩個重複括的號 ((
或 ))
。比如下面的例項
sum(i for i in range(10) if i % 2 == 0) # Output: 20
any(x == 0 for x in foo) # Output: True or False depending on foo
type(a > b for a in foo if a % 2 == 1) # Output: <class 'generator'>
我們可以用上面的來代替下面的表達,
sum((i for i in range(10) if i % 2 == 0))
any((x == 0 for x in foo))
type((a > b for a in foo if a % 2 == 1))
呼叫生成器函式會生成一個生成器物件,以後可以對其進行迭代。與其他型別的迭代器不同,生成器物件只能遍歷一次。
g1 = function()
print(g1) # Out: <generator object function at 0x1012e1888>
請注意,生成器的主體不會立即執行:當你 function()
在上面的示例中呼叫時,它只會返回生成器物件,而不去執行第一個 print
語句。這使得生成器比返回列表的函式消耗更少的記憶體,並且它能夠實現生成無限長的序列。
出於這個原因,生成器通常用於資料科學以及涉及大量資料的其他環境。另一個優點是其他程式碼可以立即使用生成器產生的值,而無需等待生成完整的序列。
但是,如果你需要多次使用生成器生成的值,並且如果生成它們的成本高於儲存,則將 list
生成的值儲存為比重新生成序列更好。有關詳細資訊,請參閱下面的重置生成器。
通常,生成器物件用於迴圈或任何需要迭代的函式中:
for x in g1:
print("Received", x)
# Output
# Received 0
# Received 1
# Received 4
# Received 9
# Received 16
# Received 25
# Received 36
# Received 49
# Received 64
# Received 81
arr1 = list(g1)
# arr1 = [], 因為上面的迴圈已經把所有的資料給用光了
g2 = function()
arr2 = list(g2) # arr2 = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
由於生成器物件是迭代器,因此可以使用 next()
函式手動迭代它們。這樣將在每次後續呼叫時逐個返回產生的值。
在這種情況下,每次對生成器呼叫 next()
時,Python 都會在生成器函式體中執行語句,直到它到達下一個 yield
語句。此時它返回 yield
命令的引數,並記住該發生點。next()
再次呼叫時將從該點恢復執行並繼續直到下一個 yield
語句。
如果 Python 到達生成器函式的末尾而不再遇到 yield
,將會引發 StopIteration
異常(這是正常的,所有迭代器都以相同的方式執行)。
g3 = function()
a = next(g3) # a becomes 0
b = next(g3) # b becomes 1
c = next(g3) # c becomes 2
...
j = next(g3) # Raises StopIteration, j remains undefined
.next()
可用於手動迭代生成值的方法。在 Python 3 中,此方法已被替換 .__next__()
。重置生成器
請記住,你只能迭代一次生成器生成的物件。如果你已經在指令碼中迭代了物件,那麼任何進一步的嘗試都會產生 None
。
如果需要多次使用生成器生成的物件,可以再次定義生成器函式並再次使用它,或者,也可以在首次使用時將生成器函式的輸出儲存在列表中。如果要處理大量資料,重新定義生成器函式將是一個不錯的選擇,因為儲存所有資料項的列表將佔用大量磁碟空間。相反,如果最初生成各個元素的成本很高,我們更願意將生成的專案儲存在列表中,以便可以重複使用它們。
迭代
生成器物件支援迭代器協議。也就是說,它提供了一個 next()
方法(Python 3.x 中為 __next__()
),用於逐步執行它,並且它的 __iter__
方法返回自己。這意味著生成器可以在任何支援通用可迭代物件的語言構造中使用。
# 簡單的 Python 2.x 中 xrange()函式的實現
def xrange(n):
i = 0
while i < n:
yield i
i += 1
# 迴圈
for i in xrange(10):
print(i) # prints the values 0, 1, ..., 9
# unpacking
a, b, c = xrange(3) # 0, 1, 2
# building a list
l = list(xrange(10)) # [0, 1, ..., 9]
next()
函式
內建的 next()
函式是一個方便的包裝函式,它可以用於從任何迭代器中獲得資料(包括生成器迭代),並提供在迭代結束時的預設值。
def nums():
yield 1
yield 2
yield 3
generator = nums()
next(generator, None) # 1
next(generator, None) # 2
next(generator, None) # 3
next(generator, None) # None
next(generator, None) # None
# .
語法是 next(iterator[, default])
。如果迭代器結束並且已經給定了預設值,則返回它。如果未提供預設值,則會引發 StopIteration
錯誤。
生成器表示式
可以使用類似於推導式的語法來建立生成器迭代器。
generator = (i * 2 for i in range(3))
next(generator) # 0
next(generator) # 2
next(generator) # 4
next(generator) # raises StopIteration
如果函式不一定需要傳遞列表給它,那可以通過在函式呼叫中放置生成器表示式來使得程式碼變得簡潔(並提高可讀性)。函式呼叫的括號隱式地使裡面的表示式成為生成器表示式。
sum(i ** 2 for i in range(4)) # 0^2 + 1^2 + 2^2 + 3^2 = 0 + 1 + 4 + 9 = 14
此外,你還可以節省記憶體,因為迭代器允許 Python 根據需要來獲取資料,而不是將整個列表 [0, 1, 2, 3]
都載入進去。