在本文中,您將學(xué)習(xí)如何使用Python生成器輕松創(chuàng)建迭代,它與迭代器和常規(guī)函數(shù)有何不同,以及為什么要使用它。
用Python構(gòu)建迭代器有很多開銷; 我們必須使用__iter__()和__next__()方法實現(xiàn)一個類,跟蹤內(nèi)部狀態(tài),在沒有要返回的值時觸發(fā)StopIteration等等。
這既冗長又違反直覺。生成器在這種情況下可以派上用場。
Python生成器是創(chuàng)建迭代器的簡單方法。我們上面提到的所有開銷都由Python的生成器自動處理。
簡而言之,生成器是一個函數(shù),它返回一個對象(迭代器),我們可以對其進行迭代(一次一個值)。
在Python中創(chuàng)建生成器非常簡單。 就像使用yield語句而不是return語句定義普通函數(shù)一樣容易。
如果一個函數(shù)包含至少一個yield語句(它可能包含其他yield或return語句),那么它就成為一個生成器函數(shù)。yield和return都將從函數(shù)返回一些值。
不同之處在于,當(dāng)return語句完全終止一個函數(shù)時,yield語句會暫停該函數(shù)保存其所有狀態(tài),然后在后續(xù)調(diào)用時繼續(xù)執(zhí)行。
這是生成器函數(shù)與常規(guī)函數(shù)的不同之處。
生成器函數(shù)包含一個或多個yield語句。
調(diào)用時,它返回一個對象(迭代器),但不會立即開始執(zhí)行。
像__iter__()和__next__()這樣的方法會自動實現(xiàn)。因此,我們可以使用next()來遍歷項目。
一旦函數(shù)產(chǎn)生了結(jié)果,函數(shù)就會暫停,控制就會轉(zhuǎn)移給調(diào)用者。
局部變量及其狀態(tài)在連續(xù)調(diào)用之間被記住。
最后,當(dāng)函數(shù)終止時,在進一步調(diào)用時會自動引發(fā)StopIteration。
這是一個示例,用于說明上述所有要點。我們有一個my_gen()由幾個yield語句命名的生成器函數(shù)。
# 一個簡單的生成器函數(shù) def my_gen(): n = 1 print('這是第一次打印') # 生成器函數(shù)包含yield語句 yield n n += 1 print('這是第二次打印') yield n n += 1 print('這是最后一次打印') yield n
解釋器中的交互式運行如下所示。在Python Shell中運行這些命令以查看輸出。
>>> # 它返回一個對象,但不立即開始執(zhí)行. >>> a = my_gen() >>> # 我們可以使用next()遍歷這些項. >>> next(a) 這是第一次打印 1 >>> # 一旦函數(shù)產(chǎn)生了結(jié)果,函數(shù)就會暫停,控制就會轉(zhuǎn)移給調(diào)用者。 >>> # 局部變量及其狀態(tài)在連續(xù)調(diào)用之間被記住。 >>> next(a) 這是第二次打印 2 >>> next(a) 這是最后一次打印 3 >>> # 最后,當(dāng)函數(shù)終止時,在進一步調(diào)用時將自動引發(fā)StopIteration。 >>> next(a) Traceback (most recent call last): ... StopIteration >>> next(a) Traceback (most recent call last): ... StopIteration
在上面的示例中要注意的一件有趣的事情是,每次調(diào)用之間都會記住變量n的值。
與普通函數(shù)不同,局部變量在函數(shù)產(chǎn)生時不會被破壞。此外,生成器對象只能迭代一次。
要重新啟動該過程,我們需要使用= my_gen()之類的東西來創(chuàng)建另一個生成器對象。
注意:最后要注意的一點是,我們可以直接將生成器與for循環(huán)一起使用。
這是因為,for循環(huán)接受一個迭代器,并使用next()函數(shù)對其進行迭代。當(dāng)StopIteration被觸發(fā)時,它會自動結(jié)束。了解如何在Python中實際實現(xiàn)for循環(huán)。
# 一個簡單的生成器函數(shù) def my_gen(): n = 1 print('這是第一次打印') # 生成器函數(shù)包含yield語句 yield n n += 1 print('這是第二次打印') yield n n += 1 print('這是最后一次打印') yield n # 使用for循環(huán) for item in my_gen(): print(item)
運行該程序時,輸出為:
這是第一次打印 1 這是第二次打印 2 這是最后一次打印 3
上面的示例用處不大,我們研究它只是為了了解背景中發(fā)生的事情。
通常,生成器函數(shù)是通過具有適當(dāng)終止條件的循環(huán)來實現(xiàn)的。
讓我們以反轉(zhuǎn)字符串的生成器為例。
def rev_str(my_str): length = len(my_str) for i in range(length - 1,-1,-1): yield my_str[i] # For循環(huán)以反轉(zhuǎn)字符串 # 輸出: # o # l # l # e # h for char in rev_str("hello"): print(char)
在此示例中,我們使用range()函數(shù)使用for循環(huán)以相反的順序獲取索引。
事實證明,此生成器函數(shù)不僅適用于字符串,還適用于其他種類的可迭代對象,例如list,tuple等。
使用生成器表達式可以輕松地動態(tài)創(chuàng)建簡單的生成器。它使建造生成器變得容易。
與lambda函數(shù)創(chuàng)建匿名函數(shù)相同,生成器表達式創(chuàng)建匿名生成器函數(shù)。
生成器表達式的語法類似于Python中的列表理解語法。但是將方括號替換為圓括號。
列表理解與生成器表達式之間的主要區(qū)別在于,雖然列表理解生成整個列表,但生成器表達式一次生成一個項目。
他們有點懶,只在需要時才生成項目。由于這個原因,生成器表達式比等價的列表理解的內(nèi)存效率要高得多。
# 初始化列表 my_list = [1, 3, 6, 10] # 使用列表理解對每個項目進行平方 # 輸出: [1, 9, 36, 100] [x**2 for x in my_list] # 同樣的事情可以使用生成器表達式來完成 # 輸出: <generator object <genexpr> at 0x0000000002EBDAF8> (x**2 for x in my_list)
上面我們可以看到生成器表達式?jīng)]有立即產(chǎn)生所需的結(jié)果。相反,它返回了一個生成器對象,該對象帶有按需生產(chǎn)的物品。
# 初始化list my_list = [1, 3, 6, 10] a = (x**2 for x in my_list) # 輸出: 1 print(next(a)) # 輸出: 9 print(next(a)) # 輸出: 36 print(next(a)) # 輸出: 100 print(next(a)) # 輸出: StopIteration next(a)
生成器表達式可以在函數(shù)內(nèi)部使用。以這種方式使用時,可以刪除圓括號。
>>> sum(x**2 for x in my_list) 146 >>> max(x**2 for x in my_list) 100
有幾個原因使生成器成為一個有吸引力的實現(xiàn)。
與它們的迭代器類對應(yīng)項相比,生成器可以以一種清晰而簡潔的方式實現(xiàn)。下面是一個使用iterator類實現(xiàn)2的冪序列的示例。
class PowTwo: def __init__(self, max = 0): self.max = max def __iter__(self): self.n = 0 return self def __next__(self): if self.n > self.max: raise StopIteration result = 2 ** self.n self.n += 1 return result
這代碼很長?,F(xiàn)在,使用生成器函數(shù)執(zhí)行相同的操作。
def PowTwoGen(max = 0): n = 0 while n < max: yield 2 ** n n += 1
由于生成器自動跟蹤細節(jié),因此簡潔明了,實現(xiàn)起來也更加簡潔。
一個普通的返回序列的函數(shù)會在返回結(jié)果之前在內(nèi)存中創(chuàng)建整個序列。如果序列中的項目數(shù)量很大,會影響效率。
而這種序列的生成器實現(xiàn)對內(nèi)存友好,因此是首選的,因為它一次只能生成一項。
生成器是表示無限數(shù)據(jù)流的絕佳媒介。無限流無法存儲在內(nèi)存中,并且由于生成器一次只生成一項,因此它可以表示無限數(shù)據(jù)流。
下面的示例可以生成所有偶數(shù)(至少在理論上)。
def all_even(): n = 0 while True: yield n n += 2
生成器可用于流水線化一系列操作。最好用一個示例來說明。
假設(shè)我們有一個著名的快餐連鎖店的日志文件。日志文件中有一個列(第4列),該列跟蹤每小時售出的比薩的數(shù)量,我們希望將其求和以得出5年內(nèi)售出的比薩的總數(shù)。
假設(shè)所有內(nèi)容都是字符串,沒有可用的數(shù)字被標(biāo)記為“ N / A”。生成器的實現(xiàn)可以如下。
with open('sells.log') as file: pizza_col = (line[3] for line in file) per_hour = (int(x) for x in pizza_col if x != 'N/A') print("Total pizzas sold = ",sum(per_hour))
這種流水線高效且易于閱讀(是的,非??幔。?。