Python 發生器

Jinku Hu 2023年1月30日
  1. 介紹
  2. 重置生成器
  3. 迭代
  4. next() 函式
  5. 生成器表示式
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

此生成器函式跟前一個生成器表示式等效,它們輸出相同的結果。

Info
所有生成器表示式都有自己的等效函式,反之亦然。

生成器表示式是函式括號內的唯一引數,則可以使用不帶括號的生成器表示式,否則的話,就會出現兩個重複括的號 (())。比如下面的例項

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
注意
在 Python 2 中,生成器物件具有 .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] 都載入進去。

作者: Jinku Hu
Jinku Hu avatar Jinku Hu avatar

DelftStack.com 創辦人。Jinku 在機器人和汽車行業工作了8多年。他在自動測試、遠端測試及從耐久性測試中創建報告時磨練了自己的程式設計技能。他擁有電氣/ 電子工程背景,但他也擴展了自己的興趣到嵌入式電子、嵌入式程式設計以及前端和後端程式設計。

LinkedIn Facebook