理解新概念
Python V2.2 中引入了迭代器的思想。唔,這並不十分正確;這種思想的“苗頭”早已出現在較老的函數 xrange() 以及檔案方法 .xreadlines() 中了。通過引入 yield 關鍵字,Python 2.2 在內部實現的許多方面推廣了這一概念,並使編程定製迭代器變得更為簡單( yield 的出現使函數轉換成產生器,而產生器反過來又返回迭代器)。
迭代器背後的動機有兩方面。將資料作為序列處理通常是最簡單的方法,而以線性順序處理的序列通常並不需要都同時實際 存在。
x*() 前兆提供了這些原理的清晰樣本。如果您想對某操作執行成千上萬次,那麼執行您的程式可能要花些時間,但該程式一般不需要佔用大量記憶體。同樣,對於許多類型的檔案,可以一行一行地處理,且不需要將整個檔案儲存體在記憶體中。最好對其它所有種類的序列也進行惰性處理;它們可能依賴於通過通道逐步到達的資料,或者依賴於一步一步執行的計算。
大多數時候,迭代器用在 for 迴圈內,就象真正的序列那樣。迭代器提供了 .next() 方法,它可以被顯式調用,但有百分之九十九的可能,您所看到的是以下行:
for x in iterator: do_something_with(x)
在對 iterator.next() 進行幕後調用而產生 StopIteration 異常時,該迴圈就被終止。順便說一下,通過調用 iter(seq) ,實際序列可以被轉換成迭代器 - 這不會節省任何記憶體,但是在下面討論的函數中它會很有用。
Python 不斷髮展的分裂性格
Python 對函數編程(FP)的觀點有點相互矛盾。一方面,許多 Python 開發人員輕視傳統的 FP 函數 map() 、 filter() 和 reduce() ,常常建議使用“列表理解”來代替它們。但完整的 itertools 模組恰恰是由與這些函數類型完全相同的函數組成的,只不過這些函數對“惰性序列”(迭代器)操作,而不是對完整的序列(列表,元組)操作。而且,Python 2.3 中沒有任何“迭代器理解”的文法,這似乎與列表理解擁有一樣的動機。
我猜想 Python 最終會產生某種形式的迭代器理解,但這取決於找到合適於它們的自然文法。同時,在 itertools 模組中,我們擁有大量有用的組合函數。大致地,這些函數中的每一個都接受一些參數(通常包含一些基礎迭代器)並返回一個新迭代器。例如,函數 ifilter() 、 imap() 和 izip() 都分別直接等同於缺少詞首 i 的內建函數。
缺少的等價函數
itertools 中沒有 ireduce() ,儘管按道理很自然地應該有這個函數;可能的 Python 實現是:
清單 1. ireduce() 的樣本實現
def ireduce(func, iterable, init=None): if init is None: iterable = iter(iterable) curr = iterable.next() else: curr = init for x in iterable: curr = func(curr, x) yield curr
ireduce() 的用例類似於 reduce() 的用例。例如,假設您想要添加某個大型檔案所包含的一列數字,但是當滿足一個條件時就停止。您可以使用以下代碼來監控正在計算的合計數:
清單 2. 添加併合計一列數
from operator import addfrom itertools import *nums = open('numbers')for tot in takewhile(condition, ireduce(add, imap(int, nums)): print "total =", tot
一個更實際的樣本可能類似於將事件流應用於有狀態物件上,例如應用到 GUI 視窗小組件上。但是即使是上述簡單樣本也顯示了迭代器組合器的 FP 風格。
基本迭代器工廠
itertools 中的所有函數都可以用純 Python 輕鬆地實現為產生器。在 Python 2.3+ 中包含該模組的要點是為一些有用的函數提供標準行為和名稱。儘管程式員可以編寫他們自己的版本,但是每個人實際建立的變體都會有點不相容。但是,另一方面是要以高效率的 C 代碼實現迭代器組合器。使用 itertools 函數將比編寫您自己的組合器稍微快一些。標準文檔顯示了每個 itertools 函數的等價純 Python 實現,所以不需要在本文中重複這些內容了。
itertools 中的函數再基本不過了 - 而且命名也完全不同 - 這樣從該模組匯入所有名稱可能就有意義了。例如,函數 enumerate() 可能明顯地出現在 itertools 中,但是它在 Python 2.3+ 中卻是一個內建函數。值得注意的是,您可以用 itertools 函數很方便地表達 enumerate() :
from itertools import *enumerate = lambda iterable: izip(count(), iterable)
讓我們首先看一下幾個 itertools 函數,它們 沒有將其它迭代器作為基礎,而完全是“從頭”建立迭代器。 times() 返回一個多次產生同一對象的迭代器;在本質上,這一能力比較有用,但它確實可以很好地替代使用過多的 xrange() 和索引變數,從而可以簡單地重複一個操作。即,不必使用:
for i in xrange(1000): do_something()
您現在就可以使用更中性的:
for _ in times(1000): do_something()
如果 times() 只有一個參數,那麼它只會重複產生 None 。函數 repeat() 類似於 times() ,但它無界地返回同一對象。不管是在包含獨立 break 條件的迴圈中還是在象 izip() 和 imap() 這樣的組合器中,這個迭代器都很有用。
函數 count() 有點類似於 repeat() 和 xrange() 的交叉。 count() 無界地返回連續整數(以可選的參數為開始)。但是,如果 count() 當前不支援溢出到現在正確的 longs,那麼您可能還是要使用 xrange(n,sys.maxint) ;它並不是完全無界的,但是對於大多數用途,它實際上是一回事。類似於 repeat() , count() 在其它迭代器組合器內部特別有用。
組合函數
我們已經順便提到了 itertools 中的幾個實際組合函數。 ifilter() 、 izip() 和 imap() 的作用就象您會期望從它們相應的序列函數上獲得的作用。 ifilterfalse() 很方便,所以您不需要去掉 lambda 和 def 中的謂詞函數(而且這還節省了大量的函數調用開銷)。但是在功能上,您可以將 ifilterfalse() 定義為(大致的情況,忽略了 None 謂詞):
def ifilterfalse(predicate, iterable): return ifilter(lambda predicate: not predicate, iterable)
函數 dropwhile() 和函數 takewhile() 根據謂詞對迭代器進行劃分。 dropwhile() 在直到滿足某個謂詞 之前忽略所產生的元素, takewhile() 在滿足某個謂詞 時就終止。 dropwhile() 跳過迭代器的不定數目的初始元素,所以它可能直到某個延遲後才開始迭代。 takewhile() 馬上開始迭代,但是如果被傳入的謂詞變為真,那麼就終止迭代器。
函數 islice() 基本上就是列表分區的迭代器版本。您可以指定開始、停止和步長,就象使用常規的片。如果給定了開始,那麼會刪除大量元素,直到被傳遞的迭代器到達滿足條件的元素為止。這是另一個我認為可以對 Python 進行改進的情形 - 迭代器最好只識別片,就象列表所做的(作為 islice() 行為的同義字)。
最後一個函數 starmap() 在 imap() 基礎上略微有些變化。如果這個作為參數傳遞的函數擷取多個參數,那麼被傳遞的 iterable 會產生大小適合的元組。這基本上與包含多個被傳入 iterable 的 imap() 相同,只不過它包含先前與 izip() 結合在一起的 iterables 集合。
深入探討
itertools 中包含的函數是一個很好的開始。不使用其它函數,只用這些函數就可以讓 Python 程式員更輕鬆地利用和組合迭代器。一般說來,迭代器的廣泛使用對 Python 的未來無疑是很重要的。但是除了過去所包含的內容以外,我還要對該模組的將來更新提幾點建議。您可以立即很方便地使用這些函數 - 當然,如果它們是後來被包含進來的,那麼名稱或介面細節會有所不同。
一種可能會很通用的類別是一些將多個 iterable 作為參數,隨後從每個 iterable 產生單獨元素的函數。與此相對照的是, izip() 產生元素元組,而 imap() 產生從基本元素計算而來的值。我頭腦中很清晰的兩個安排是 chain() 和 weave() 。第一個在效果上類似於序列共置(但是有點惰性)。即,在您可能使用的純序列中,例如:
for x in ('a','b','c') + (1, 2, 3): do_something(x)
對於一般的 iterables,您可以使用:
for x in chain(iter1, iter2, iter3): do_something(x)
Python 實現是:
清單 3. chain() 的樣本實現
def chain(*iterables): for iterable in iterables: for item in iterable: yield item
使用 iterables,您還可以通過使它們分散排列來組合幾個序列。還沒有任何對序列執行這樣相同操作的內建文法,但是 weave() 本身也非常適用於完整的序列。下面是可能的實現(Magnus Lie Hetland 提出了 comp.lang.python 的類似函數):
清單 4. weave() 的樣本實現
def weave(*iterables): "Intersperse several iterables, until all are exhausted" iterables = map(iter, iterables) while iterables: for i, it in enumerate(iterables): try: yield it.next() except StopIteration: del iterables[i]
讓我來示範一下 weave() 的行為,因為從實現上看不是很明顯:
>>> for x in weave('abc', xrange(4), [10,11,12,13,14]):... print x,...a 0 10 b 1 11 c 2 12 13 3 14
即使一些迭代器到達終點,但其餘迭代器會繼續產生值,直到在某一時刻產生了所有可用的值為止。
我將另外只提出一個可行的 itertools 函數。提出這個函數主要是受到了構思問題的函數編程方法的啟發。 icompose() 與上面提出的函數 ireduce() 存在某種對稱。但是在 ireduce() 將值的(惰性)序列傳遞給某個函數併產生每個結果的地方, icompose() 將函數序列應用於每個前趨函數的傳回值。可以把 ireduce() 用於將事件序列傳遞給長期活動的對象。而 icompose() 可能將對象重複地傳遞給返回新對象的賦值函數。第一種方法是相當傳統的考慮事件的 OOP 方法,而第二種的思路更接近於 FP。
以下是可能的 icompose() 實現:
清單 5. icompose() 的樣本實現
def icompose(functions, initval): currval = initval for f in functions: currval = f(currval) yield currval
結束語
迭代器 - 被認為是惰性序列 - 是功能強大的概念,它開啟了 Python 編程的新樣式。但是在只把迭代器當作資料來源與把它作為一種序列來考慮之間存在著微妙的差別。這兩種想法本質上哪一種都不見得比另一種更正確,但是後者開創了操作編程事件的一種組合性的簡略表達方法。 itertools 中的組合函數(尤其是它可能產生的一些類似於我建議的函數)接近於編程的聲明樣式。對我而言,這些聲明樣式一般都更不易出錯且更強大。