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]都載入進去。