Python 3 之 生成器詳解

—— 生成器 ——————————————————————

如今Python對延遲提供更多的支援——它提供了工具在需要的時候才產生結果,而不是立即產生結果。特別地,有兩種語言結構儘可能地延遲結果創建。

  • 生成器函數:編寫為常規的def語句,但是使用yield語句一次返回一個結果,在每個結果之間掛起和繼續它們的狀態。
  • 生成器表達式:類似於列表解析,但是,它們返回按需產生結果的一個對象,而不是構建一個結果列表。

由於二者都不會一次性構建一個列表,它們節省了記憶體空間,並且允許計算時間分散到各個結果請求。我們將會看到,這二者最終都通過實現我們在前面介紹的迭代協議來執行它們延遲結果的魔術。

生成器函數: yield VS return

我們已經學習了編寫接收輸入參數並立即送回單個結果的常規函數。然而,也有可能來編寫可以送回一個值並隨後從其退出的地方繼續的函數。這樣的函數叫做生成器函數,因為它們隨著時間產生值的一個序列。

一般來說,生成器函數和常規函數一樣,並且,實際上也是用常規的def語句編寫的,然而,當創建時,它們自動實現迭代協議,以便可以出現在迭代背景中。

狀態掛起

和返回一個值並退出的常規函數不同,生成器函數自動在生成值的時刻掛起 並 繼續函數的執行。因此,它們對於提前計算整個一系列值以及在雷總手動保存和恢復狀態都很有用。由於生成器函數在掛起時保存的狀態包含它們的整個本地作用域,當函數恢復時,它們的本地變數保持了資訊並且使其可用

生成器函數和常規函數之間的主要程式碼不同之處在於,生成器yields一個值,而不是返回一個值。yield語句掛起該函數並向調用者發送回一個值,但是,保留足夠的狀態以使得函數能夠從它離開的地方繼續。當繼續時,函數在上一個yield返回後立即繼續執行。從函數的角度來看,則允許其程式碼隨著時間產生一系列的值,而不是一次計算他們並在諸如列表的內容中送回它們。

迭代協議整合

要真正地理解生成器函數,我們需要知道,它們與Python中的迭代協議的概念密切相關。正如我們看到的,可迭代的對象定義了一個__next__方法,它要麼返回迭代中的下一項,或者引發一個特殊的StopIteration異常來終止迭代。一個對象的迭代器用iter內置函數接收。

如果支援該協議的話,Python的for循環以及其他的迭代背景,使用這種迭代協議來遍歷一個序列或值生成器; 如果不支援,迭代返回去重複索引序列。

要支援這一協議,函數包含一條yield語句,該語句特別編譯為生成器。當調用時,它們返回一個迭代器對象,該對象支援用一個名為__next__的自動創建的方法來繼續執行的介面。生成器函數也可能有一條return語句,總是在def語句塊的末尾,直接終止值的生成。 從技術上將,尅在任何常規函數退出執行之後,引發一個StopIteration異常來實現。 從調用者的角度來看,生成器的__next__方法繼續函數並且運行到下一個yield結果返回或引發一個StopIteration異常。

直接效果就是生成器函數,編寫為包含yield語句的def語句,自動地支援迭代協議,並且由此可能用在任何迭代環境中隨著時間並根據需要產生結果。

生成器函數應用

為了講清楚基礎知識,請看如下程式碼,它定義了一個生成器函數,這個函數將會用來不斷地生成一些列的數字的平方。

>>> def gensquares(N):  	for i in range(N):  		yield i ** 2

這個函數在每次循環時都會產生一個值,之後將其返還給它的調用者。當它被暫停後,它的上一個狀態保存了下來,並且在yield語句之後控制器馬上被回收。例如,當用在一個for循環中時,在循環中每一次完成函數的yield語句後,控制權都會返還給函數。

>>> for i in gensquares(5):  	print(i, end=" : ")    0 : 1 : 4 : 9 : 16 :

為了終止生成值,函數可以使用給一個無值的返回語句,或者在函數體最後簡單的讓控制器脫離。

如果想要看看在for裡面發生了什麼,直接調用一個生成器函數:

>>> x = gensquares(4)  >>> x  <generator object gensquares at 0x0000014EF59FEDB0>

得到的是一個生成器對象,它支援迭代器協議,也就是所生成器對象有一個__next__方法,它可以開始這個函數,或者從它上次yield值後的地方恢復,並且在得到一系列的值的最後一個時,產生StopIteration異常。為了方便起見,next(x)內置函數為我們調用一個對象的X.__next__()方法:

>>> next(x)        # 類似py3 的 x.__next__()  0  >>> next(x)        # 在py2 中類似的方法為 x.next() 或 next()  1  >>> next(x)  4  >>> next(x)  9  >>> next(x)  Traceback (most recent call last):    File "<pyshell#52>", line 1, in <module>      next(x)  StopIteration

正如前面學過的,for循環(以及其他的迭代環境)以同樣的方式與生成器一起工作:通過重複的__next__方法,知道捕獲一個異常。如果一個不支援這種協議的對象進行這樣迭代,for循環會使用索引協議進行迭代

注意在這個例子中,我們能夠簡單地一次就構建一個所獲得的值的列表。

>>> def buildsquares(n):  	res = []  	for i in range(n): res.append(i ** 2)  	return res    >>> for x in buildsquares(5): print(x, end = " : ")    0 : 1 : 4 : 9 : 16 :

對於這樣的例子,我們還能夠使用for循環、map或者列表解析的技術來實現。

>>> for x in [n ** 2 for n in range(5)]:  	print(x, end=" : ")    0 : 1 : 4 : 9 : 16 :       >>> for  x in map((lambda n: n ** 2), range(5)):  	print(x, end=" : ")    0 : 1 : 4 : 9 : 16 :

儘管如此,生成器在記憶體使用和性能方面都更好。它們允許函數避免臨時再做所有的工作,當結果的列表很大或者在處理每一個結果都需要很多時間時,這一點尤其有用。生成器將在loop迭代中處理一系列值的時間分布開來。

儘管如此,對於更多高級的應用,它們提供了一個更簡單的替代方案來手動將類的對象保存到迭代中的狀態。 有了生成器,函數變數就能進行自動的保存和恢復。

擴展生成器函數協議:send 和 next

在Python2.5中,生成器函數協議中增加了一個send方法。send方法生成一系列結果的下一個元素,這一點就像__next__方法一樣,但是它也提供了一種調用者與生成器之間進行通訊的方法,從而能夠影響它的操作。

從技術上來說,yield現在是一個表達式的形式,可以返回傳入的元素來發送,而不是一個語句[儘管無論哪種叫法都可以:作為yield X 或者 A = (yield X)]。表達式必須包括在括弧中,除非它是賦值語句右邊的唯一一項。 例如,X = yield Y沒問題,就如同 X = (yield Y) + 42。

當使用這一額外的協議時,值可以通過調用G.send(value)發送給一個生成器G。之後恢復生成器的程式碼,並且生成器中的yield表達式返回了為了發送而傳入的值。如果提前調用了正常的G.__next__()方法(或者其對等的next(G)),yield返回None。例如:

>>> def gen():  	for i in range(10):  		X = yield i  		print(X)    >>> G = gen()  >>> next(G)        # next() 開始生成器  0  >>> G.send(77)     # 高級的的send方法 發送參數給生成器表達式  77  1  >>> G.send(88)  88  2  >>> next(G)        # 返回None  None  3

例如,用send方法,編寫一個能夠被它的調用者終止的生成器。此外,在2.5版中,生成器還支援throw(type)的方法,它將生成器內部最後一個yield時產生一個異常以及一個close方法,它會在生成器內部產生一個終止迭代的新的GeneratorExit異常。這些都是我們這裡不會深入學習的一些高級特性; 需要了解的請查看Python的標準庫以獲得更多的細節。

注意,儘管Python 3提供了一個next(X)方便的內置函數,它會調用一個對象的X.__next__方法,但是,其他的生成器方法,例如send,必須直接作為生成器對象的方法來調用(例如,G.send(X))。這麼做是有意義的,你要知道,這些額外的方法只是在內置的生成器對象上實現,而__next__方法應用於所有的可迭代對象(包括內置類型和用戶定義的類)。

生成器表達式:迭代器遇到列表解析

在最新版本的Python中,迭代器和列表解析的概念形成了這種語言的一個新的特性,生成器表達式。 從語法上來講,生成器表達式就像一般的列表解析一樣,但是它們是括在圓括弧中而不是方括弧中的。

>>> [x ** 2 for x in range(4)]  [0, 1, 4, 9]    >>> (x ** 2 for x in range(4))        # 生成器表達式  <generator object <genexpr> at 0x0000014EF59FEDB0>

實際上,至少在一個函數的基礎上,編寫一個列表解析基本上等同於:在一個list內置調用中包含一個生成器表達式以迫使其一次生成列表中所有的結果。

>>> list(x ** 2 for x in range(4))  [0, 1, 4, 9]

儘管如此,從執行過程上來講,生成器表達式很不相同:不是在記憶體中構建結果,而是返回一個生成器對象,這個對象將會支援迭代協議並在任意的迭代語境的操作中。

>>> G = (x ** 2 for x in range(4))  >>> next(G)  0  >>> next(G)  1  >>> next(G)  4  >>> next(G)  9  >>> next(G)  Traceback (most recent call last):    File "<pyshell#99>", line 1, in <module>      next(G)  StopIteration

我們一般不會機械地使用next迭代器來操作生成器表達式,因為for循環會自動觸發。

>>> for num in (x ** 2 for x in range(4)):  	print("%s, %s" % (num, num / 2.0))    0, 0.0  1, 0.5  4, 2.0  9, 4.5

實際上,每一個迭代的語境都會這樣,包括sum、map 和 sorted等內置函數,以及在前面涉及的其他迭代語境,例如 any、all 和 list內置函數等。

注意,如果生成器表達式是在其他的括弧之內,就像在那些函數調用之中,這種情況下,生成器自身的括弧就不是必須的了。 儘管這樣,在下面第二個sorted調用中,還是需要額外的括弧。

>>> sum(x ** 2 for x in range(4))  14  >>> sorted(x ** 2 for x in range(4))  [0, 1, 4, 9]  >>> sorted((x ** 2 for x in range(4)), reverse=True)  [9, 4, 1, 0]    >>> import math  >>> list( map(math.sqrt, (x ** 2 for x in range(4))) )  [0.0, 1.0, 2.0, 3.0]

生成器表達式大體上可以認為是記憶體空間的優化,它們不需要像方括弧的列表解析一樣,一次構造出整個列表。它們在實際中運行起來可能稍慢一些,所以它們可能對於非常大的結果集合的運算來說是最優的選擇。關於性能的更權威的評價,必須等到最後編寫計時腳本的時候給出。

生成器函數 VS 生成器表達式

有趣的是,同樣的迭代旺旺可以用一個生成器函數或一個生成器表達式編寫。例如,如下的生成式表達式,把一個字元串中的每個字母重複4次。

>>> G = (c * 4 for c in "SPAM")  >>> list(G)  ['SSSS', 'PPPP', 'AAAA', 'MMMM']

等價的生成器函數需要略微多一些的程式碼,但是,作為一個多語句的函數,如果需要的話,它將能夠編寫更多的邏輯並使用更多的狀態資訊。

>>> def timesfour(S):  	for c in S:  		yield c * 4    >>> G = timesfour("spam")  >>> list(G)  ['ssss', 'pppp', 'aaaa', 'mmmm']

表達式和函數支援自動迭代和手動迭代……前面的列表自動調用迭代,如下的迭代手動進行。

>>> G = (c * 4 for c in "SPAM")  >>> i = iter(G)  >>> next(i)  'SSSS'  >>> next(i)  'PPPP'    >>> G = timesfour('spam')  >>> I = iter(G)  >>> next(I)  'ssss'  >>> next(I)  'pppp'

注意,我們使得這裡的新的生成器再次迭代,正如下一小節所介紹的,生成器是單次迭代器。

生成器是單迭代器對象

生成器函數和生成器表達式自身都是迭代器,並由此只支援一次活躍迭代……不像一些內置類型,我們無法有在結果集中位於不同位置的多個迭代器。例如,使用前面小節的生成器表達式,一個生成器的迭代器是生成器之神(實際上,在一個生成器上調用iter沒有任何效果)。

>>> G = (c * 4 for c in "SPAM")  >>> iter(G) is G  True

如果你手動地使用多個迭代器來迭代結果流,它們將會指向相同的位置。

>>> G = (c * 4 for c in "SPAM")        # 新生成器表達式  >>> I1 = iter(G)  >>> next(I1)  'SSSS'  >>> next(I1)  'PPPP'  >>> I2 = iter(G)                        # ----  >>> next(I2)  'AAAA'

此外,一旦任何迭代器運行到完成,所偶的迭代器都將用盡,我們必須產生一個新的生成器以再次開始。

>>> list(I1)                            # 自動迭代  ['MMMM']  >>> next(I2)                            # I2的手動迭代  Traceback (most recent call last):    File "<pyshell#156>", line 1, in <module>      next(I2)  StopIteration                           # 異常  >>> I3 = iter(G)                        # 生成新的迭代器(其實不會生成新的)  >>> next(I3)  Traceback (most recent call last):    File "<pyshell#158>", line 1, in <module>      next(I3)  StopIteration                            # 仍舊迭代異常  >>> I3 = iter(c * 4 for c in "SPAM")     # 新的迭代器  >>> next(I3)                             # 開始迭代  'SSSS'

對於生成器函數來說,也是如此,如下的基於語句的def等價形式只支援一個活躍的生成器並且在一次迭代之後用盡。

>>> def timesfour(S):  	for c in S:  		yield c * 4    >>> G = timesfour("spam")  >>> iter(G) is G  True  >>> I1, I2 = iter(G), iter(G)  >>> next(I1)  'ssss'  >>> next(I1)  'pppp'  >>> next(I2)  'aaaa'

這與某些內置類型的行為不同,它們支援多個迭代器並且在一個活動迭代器中傳遞並反映它們的原處修改。

>>> L = [1, 2, 3, 4]  >>> I1, I2 = iter(L), iter(L)  >>> next(I1)  1  >>> next(I1)  2  >>> next(I2)  1  >>> del L[2:]  >>> next(I1)  Traceback (most recent call last):    File "<pyshell#180>", line 1, in <module>      next(I1)  StopIteration

當我們開始別寫基於類的迭代器時,我們將看到,由我們來決定想要為自己的對象支援多個迭代器。

用迭代工具模擬zip 和 map

要說明應用迭代工具的能力,讓我們來看一些高級用例。一旦你了解了列表解析、生成器 和 其他的迭代工具,就知道模擬眾多的Python的函數式內置工具既直接又很有益。

例如,我們已經看到了內置的zip和map函數如何組合可迭代對象和映射函數。使用多個序列參數,map以與zip配對元素相同的方式,把函數映射到取自每個序列的元素。

>>> S1 = "abc"  >>> S2 = "xyz123"  >>> list(zip(S1, S2))  [('a', 'x'), ('b', 'y'), ('c', 'z')]    >>> list(zip([-2, -1, 0, 1, 2]))  [(-2,), (-1,), (0,), (1,), (2,)]     >>> list(zip([1, 2, 3], [2, 3, 4, 5]))  [(1, 2), (2, 3), (3, 4)]     >>> list(map(abs, [-2, -1, 0, 1, 2]))  [2, 1, 0, 1, 2]    >>> list(map(pow, [1, 2, 3], [2, 3, 4, 5]))  [1, 8, 81]

儘管它們用於不同的目的,如果你研究這些示例足夠長的時間,可能會注意到zip結果和執行map的函數參數之間的一種關係,下面的例子說明這種關係。

編寫自己的map(func, …)

儘管map和zip內置函數快速而方便,總是可以在自己的程式碼中模擬它們。例如,在之前,我們看到了一個函數針對單個的序列參數來模擬map內置函數。針對多個序列的時候也並不會費太多工夫就可以像內置函數那樣操作。

>>> def mymap(func, *seqs):  	res = []  	for args in zip(*seqs):  		res.append(func(*args))  	return res    >>> print(mymap(abs, [-2, -1, 0, 1, 2]))  [2, 1, 0, 1, 2]  >>> print(mymap(pow, [1, 2, 3], [2, 3, 4, 5]))  [1, 8, 81]

這個版本很大程度上依賴於特殊的*args參數傳遞語法。它收集多個序列(實際上,是可迭代對象)參數,將其作為zip參數解包以便組合,然後成對的zip結果解包作為參數以便傳入到函數。也就是說,我們在使用這樣的一個事實,zip是map中的一個基本嵌套操作。最後的測試程式碼對一個序列和兩個序列都應用了這個函數,以產生這一輸入(我們可以用內置的map得到同樣的輸出)。

然而,實際上,前面的版本展示了經典的列表解析模式,在一個for循環中構建操作結果的一個列表。我們可以更精簡地編寫自己的map,作為單行列表解析的對等體。

>>> def mymap(func, *seqs):  	return [func(*args) for args in zip(*seqs)]    >>> print(mymap(abs, [-2, -1, 0, 1, 2]))  [2, 1, 0, 1, 2]  >>> print(mymap(pow, [1, 2, 3], [2, 3, 4, 5]))  [1, 8, 81]

當這段程式碼運行的時候,結果與前面相同,但是,這段程式碼更加精鍊並且可能運行的更快。之前的mymap版本一次性構建結果列表,並且對於較大的列表來說,這可能浪費記憶體。既然我們知道了生成器函數和表達式,重新編碼這兩種替代方案來根據需求產生結果是很容易的。

>>> def mymap(func, *seqs):            # 生成器函數版本  	res = []  	for args in zip(*seqs):  		yield func(*args)    >>> def mymap(func, *seqs):            # 生成器表達式版本  	return (func(*args) for args in zip(*seqs))

這些版本產生同樣的結果,但是返回設計用來支撐迭代協議的生成器。第一個版本每次yield一個結果,第二個版本返回一個生成器表達式的結果來做同樣的事情,如果我們把它們包含到一個list調用中迫使它們一次生成所有的值,它們會產生同樣的結果。

>>> print(list(mymap(abs, [-2, -1, 0, 1, 2])))  [2, 1, 0, 1, 2]  >>> print(list(mymap(pow, [1, 2, 3], [2, 3, 4, 5])))  [1, 8, 81]

這裡並沒有做什麼實際工作,知道list調用迫使生成器運行,通過激活迭代協議而進行。生成器由這些函數自身返回,也由它們說使用的Python 3.x式的zip內置函數返回,根據需要產生結果。

編寫自己的zip(…)和map(None, …)

當然,目前給出的示例中的很多魔力在於,它們使用zip內置函數來配對來自多個序列的參數。我們也注意到,我們的map近似版確實是模擬了Python 3.x的map的行為,它們從最短的序列的長度處截斷,並且,當長度不同的時候,它們不支援補充結果的思路,就像Python 2.x中帶有一個None參數的map所做的一樣:

>>> map(None, [1, 2, 3], [2, 3, 4, 5])            # Python 2.6  [(1, 2), (2, 3), (3, 4), (None, 5)]  >>> map(None, 'abc', 'xyz123')                    # Python 2.6  [('a', 'x'), ('b', 'y'), ('c', 'z'), (None, '1'), (None, '2'), (None, '3')]

使用迭代工具,我們可以編寫近似版來模擬斷的zip和Python 2.6的補充的map,這些其實在程式碼上近乎是相同的:

>>> def myzip(*seqs):  	seqs = [list(S) for S in seqs]  	res = []  	while all(seqs):  		res.append(tuple(S.pop(0) for S in seqs))  	return res    >>> def mymapPad(*seqs, pad=None):  	seqs = [list(S) for S in seqs]  	res = []  	while any(seqs):  		res.append(tuple((S.pop(0) if S else pad) for S in seqs))  	return res    >>> S1, S2 = 'abc', 'xyz123'  >>> print(myzip(S1, S2))  [('a', 'x'), ('b', 'y'), ('c', 'z')]  >>> print(mymapPad(S1, S2))  [('a', 'x'), ('b', 'y'), ('c', 'z'), (None, '1'), (None, '2'), (None, '3')]  >>> print(mymapPad(S1, S2, pad=99))  [('a', 'x'), ('b', 'y'), ('c', 'z'), (99, '1'), (99, '2'), (99, '3')]

這裡編寫的函數可以在任何類型的可迭代對象上運行,因為它們通過list內置函數來運行自己的參數以迫使結果生成(例如,文件像參數一樣工作,此外,序列像字元串一樣)。注意這裡的all和any內置函數的使用,如果一個可迭代對象中的所有或任何元素為True(或者對等的為非空),它們分別返回True。當列表中的任何或所有參數在刪除後變成了空,這些內置函數將用來停止循環。

還要注意Python 3.x的keyword-only參數pad,和Python 2.6的map不同,我們的版本將允許指定任何補充的對象(如果你使用Python 2.6,使用一個**kargs形式來支援這一選項)。當這些函數運行的時候,列印出如下的結果……一個zip和兩個補充的map。

[('a', 'x'), ('b', 'y'), ('c', 'z')]  [('a', 'x'), ('b', 'y'), ('c', 'z'), (None, '1'), (None, '2'), (None, '3')]  [('a', 'x'), ('b', 'y'), ('c', 'z'), (99, '1'), (99, '2'), (99, '3')]

這些函數不能夠用於列表解析轉換,因為它們的循環太具體了。然而,和前面一樣,既然我們的zip和map近似版構建並返回列表,用yield將它們轉換為生成器以便它們每個都是每次返回結果中的一項,這還是很容易做到的。結果和前面的相同,但是,我們需要再次使用list來迫使該生成器產生其值以供顯示。

>>> def myzip(*seqs):  	seqs = [list(S) for S in seqs]  	res = []  	while all(seqs):  		yield tuple(S.pop(0) for S in seqs)    >>> def mymapPad(*seqs, pad=None):  	seqs = [list(S) for S in seqs]  	res = []  	while any(seqs):  		yield tuple((S.pop(0) if S else pad) for S in seqs)    >>> S1, S2 = 'abc', 'xyz123'  >>> print(myzip(S1, S2))  <generator object myzip at 0x00000161CDB15FC0>  >>> print(mymapPad(S1, S2))  <generator object mymapPad at 0x00000161CDB15F68>  >>> print(mymapPad(S1, S2, pad=99))  <generator object mymapPad at 0x00000161CDB15FC0>

最後,這裡是我們的zip和map模擬器的替代實現……下面的版本不是使用pop方法從列表中刪除參數,而是通過計算最小和最大參數長度來完成其工作,有了這些長度,很容易編寫嵌套的列表解析來遍歷參數索引範圍。

>>> def myzip(*seqs):  	minlen = min(len(S) for S in seqs)  	return [tuple(S[i] for S in seqs) for i in range(minlen)]    >>> def mymapPad(*seqs, pad=None):  	maxlen = max(len(S) for S in seqs)  	index = range(maxlen)  	return [tuple((S[i] if len(S) > i else pad) for S in seqs) for i in index]    >>> S1, S2  = 'abc', 'xyz123'  >>> print(myzip(S1, S2))  [('a', 'x'), ('b', 'y'), ('c', 'z')]  >>> print(mymapPad(S1, S2))  [('a', 'x'), ('b', 'y'), ('c', 'z'), (None, '1'), (None, '2'), (None, '3')]  >>> print(mymapPad(S1, S2, pad=99))  [('a', 'x'), ('b', 'y'), ('c', 'z'), (99, '1'), (99, '2'), (99, '3')]

由於這些程式碼使用len和索引,它們假設參數是序列或類似的,而不是任意的可迭代對象。這裡,外圍的解析遍歷參數索引範圍,內部的解析(傳遞到元組)遍歷傳入的序列以並列地提取參數。當它們運行時,結果和前面相同。

更有趣的是,生成器和迭代器似乎在這個例子中泛濫。傳遞給min 和 max的參數是生成器表達式,它在嵌套和解析開始迭代之前運行完成。此外,嵌套的列表解析使用了兩個層級的延遲運算……Python 3.x的range內置函數是一個可迭代對象,就像生成器表達式參數對元組。

實際上,這裡沒有產生結果,知道列表解析的方括弧要求放入到結果列表中的值……它們迫使解析和生成器運行。為了把這些函數自身轉換為生成器而不是列表構建起,使用圓括弧而不是方括弧。zip的例子如下所示:

>>> def myzip(*seqs):  	minlen = min(len(S) for S in seqs)  	return (tuple(S[i] for S in seqs) for i in range(minlen))    >>> print(list(myzip(S1, S2)))  [('a', 'x'), ('b', 'y'), ('c', 'z')]

在這個例子中,它用一個list調用來激活生成器和迭代器以產生它們自己的結果。自己體驗這些來了解更多內容。 進一步開發替代的編碼留作一個建議練習。

 為什麼你會留意:單次迭代

在前面,我們看到了一些內置函數(如map)如何只支援一個單個的遍歷,並且在發生之後為空,我提過會給出一個示例展示這在實際中是如何變得微妙而重要的。現在,已經學習了關於迭代話題的許多內容,我可以兌現這個承諾了。考慮到本篇的zip模擬示例的更優替代程式碼,該示例從Python手冊中高端一個例子改編而來。

>>> def myzip(*args):  	iters = map(iter, args)  	while iters:  		res = [next(i) for i in iters]  		yield tuple(res)

由於這段程式碼使用iter和next,它對任何類型的可迭代對象都有效。注意,當這個參數的迭代器之一用盡時,沒有任何理由捕獲由這個解析內的next(it)來引發的StopIteration——允許它傳遞會終止這個生成器函數,並且與一條return語句具有相同的效果。如果至少傳遞了一個參數的話,while iters:對於循環來說足夠了,並且,避免了無限循環(列表解析將總是返回一個空的列表):

這段程式碼在Python 2.6中也工作的很好,如下所示:

>>> list(myzip('abc', 'lmnop'))  [('a', 'l'), ('b', 'm'), ('c', 'n')]

但是,在Python 3.x中,它陷入了一個無限循環中並失效。

>>> list(myzip('abc', 'lmnop'))

因為Python 3.x的map返回一個單次可迭代對象,而不是像Python 2.6中那樣的一個列表。在Python 3.x中,只要我們在循環中運行了一次列表解析,iters將會永遠為空(並且res將會是[])。為了使其在Python 3.x下正常工作,我們需要使用list內置函數來創建一個支援多次迭代的對象:

>>> def myzip(*args):  	iters = list(map(iter, args))  	while iters:  		res = [next(i) for i in iters]  		yield tuple(res)    >>> list(myzip('abc', 'lmnop'))  [('a', 'l'), ('b', 'm'), ('c', 'n')]

自己運行並跟蹤其操作。這裡要記住的是:在Python3.x中把map調用放入到list調用中不僅是為了顯示。

內置類型和類中的值生成

最後,儘管我們在本篇中關注自己編寫值生成器,別忘了,很多內置的類型以類似的方式工作……正如我們在之前看到的一樣,例如,字典擁有在每次迭代中產生鍵的迭代器。

>>> D = {'a':1, 'b':2, 'c':3}  >>> x = iter(D)  >>> next(x)  'a'  >>> next(x)            # 這裡並不一定就是創建字典時候的第二個鍵,因為字典是無序的。  'b'

和手動編寫的生成器所產生的值一樣,字典鍵也可以手動迭代,或者使用包括for循環、map調用、列表解析和我們在前面介紹的很多其他環境等在內的自動迭代工具。

>>> for key in D:  	print(key, D[key])    a 1  b 2  c 3

正如我們所看到的,在文件迭代器中,Python簡單地載入了一個文件的行。

>>> for line in open('NEWS.txt'):  	print(line, end='')      +++++++++++  Python News  +++++++++++    What's New in Python 3.5.1 final?  =================================  .....

儘管內置類型迭代器綁定到了一個特定類型的值生成,概念與我們使用表達式和函數編寫的生成器是類似的。像for循環這樣的迭代環境接受任何的可迭代對象,不管是用戶定義的還是內置的。

儘管這超出了本篇的討論範圍,還是可能用遵守迭代協議的類來實現任意的用戶定義的生成器對象。這樣的類定義了一個特別的__iter__方法,它由內置的iter函數調用,將返回一個對象,該對象有一個__next__方法,該方法由next內置函數調用(一個__getitem__索引方法作為迭代的退而求其次的選項也是可以的)。

從這樣的一個類創建的實例對象,看做是可迭代的,並且可以用在for循環和所有其他的迭代環境中。然而,有了類,我們可以訪問比其他生成器構造所能提供的更豐富的邏輯和數據結構選型。迭代器的內容不會真正結束,直到我們了解到它如何映射到類。

.