1. 程式人生 > >Python高階特性(1):Iterators、Generators和itertools

Python高階特性(1):Iterators、Generators和itertools

【譯註】:作為一門動態指令碼語言,Python對程式設計初學者而言很友好,豐富的第三方庫能夠給使用者帶來很大的便利。而Python同時也能夠提供一些高階的特性方便使用者使用更為複雜的資料結構。本系列文章共有三篇,本文是系列的第一篇,將會介紹迭代器、生成器以及itertools模組的相關用法。由於作者 Sahand Saba 列舉的示例中有諸多專業的數學相關內容,因此翻譯中有諸多不妥之處請大家指出,非常感謝。

對數學家來說,Python這門語言有著很多吸引他們的地方。舉幾個例子:對於tuple、lists以及sets等容器的支援,使用與傳統數學類似的符號標記方式,還有列表推導式這樣與數學中集合推導式和集的結構式(set-builder notation)很相似的語法結構。

另外一些很吸引數學愛好者的特性是Python中的iterator(迭代器)、generator(生成器)以及相關的itertools包。這些工具幫助人們能夠很輕鬆的寫出處理諸如無窮序列(infinite sequence)、隨機過程(stochastic processes)、遞推關係(recurrence relations)以及組合結構(combinatorial structures)等數學物件的優雅程式碼。本文將涵蓋我關於迭代器和生成器的一些筆記,並且有一些我在學習過程中積累的相關經驗。

Iterators

迭代器(Iterator)是一個可以對集合進行迭代訪問的物件。通過這種方式不需要將集合全部載入記憶體中,也正因如此,這種集合元素幾乎可以是無限的。你可以在Python官方文件的“迭代器型別(Iterator Type)”部分找到相關文件。

讓我們對定義的描述再準確些,如果一個物件定義了__iter__方法,並且此方法需要返回一個迭代器,那麼這個物件就是可迭代的(iterable)。而迭代器是指實現了__iter__以及next(在Python 3中為__next__)兩個方法的物件,前者返回一個迭代器物件,而後者返回迭代過程的下一個集合元素。據我所知,迭代器總是在__iter__方法中簡單的返回自己(self),因為它們正是自己的迭代器。

一般來說,你應該避免直接呼叫__iter__以及next方法。而應該使用for或是列表推導式(list comprehension),這樣的話Python能夠自動為你呼叫這兩個方法。如果你需要手動呼叫它們,請使用Python的內建函式iter以及next,並且把目標迭代器物件或是集合物件當做引數傳遞給它們。舉個例子,如果c是一個可迭代物件,那麼你可以使用iter(c)來訪問,而不是c.__iter__(),類似的,如果a是一個迭代器物件,那麼請使用next(a)而不是a.next()來訪問下一個元素。與之相類似的還有len的用法。

說到len,值得注意的是對迭代器而言沒必要去糾結length的定義。所以它們通常不會去實現__len__方法。如果你需要計算容器的長度,那麼必須得手動計算,或者使用sum。本文末,在itertools模組之後會給出一個例子。

有一些可迭代物件並不是迭代器,而是使用其他物件作為迭代器。舉個例子,list物件是一個可迭代物件,但並不是一個迭代器(它實現了__iter__但並未實現next)。通過下面的例子你可以看到list是如何使用迭代器listiterator的。同時值得注意的是list很好地定義了length屬性,而listiterator卻沒有。

1234567891011121314151617181920 >>>a=[1,2]>>>type(a)<type'list'>>>>type(iter(a))<type'listiterator'>>>>it=iter(a)>>>next(it)1>>>next(it)2>>>next(it)Traceback(most recent call last):File"<stdin>",line1,in<module>StopIteration>>>len(a)2>>>len(it)Traceback(most recent call last):File"<stdin>",line1,in<module>TypeError:objectof type'listiterator' has no len()

當迭代結束卻仍然被繼續迭代訪問時,Python直譯器會丟擲StopIteration異常。然而,前述中提到迭代器可以迭代一個無窮集合,所以對於這種迭代器就必須由使用者負責確保不會造成無限迴圈的情況,請看下面的例子:

12345678910 classcount_iterator(object):n=0def __iter__(self):returnselfdef next(self):y=self.nself.n+=1returny

下面是例子,注意最後一行試圖將一個迭代器物件轉為list,這將導致一個無限迴圈,因為這種迭代器物件將不會停止。

12345678910 >>>counter=count_iterator()>>>next(counter)0>>>next(counter)1>>>next(counter)2>>>next(counter)3>>>list(counter)# This will result in an infinite loop!

最後,我們將修改以上的程式:如果一個物件沒有__iter__方法但定義了__getitem__方法,那麼這個物件仍然是可迭代的。在這種情況下,當Python的內建函式iter將會返回一個對應此物件的迭代器型別,並使用__getitem__方法遍歷list的所有元素。如果StopIteration或IndexError異常被丟擲,則迭代停止。讓我們看看以下的例子:

123456 classSimpleList(object):def __init__(self,*items):self.items=itemsdef __getitem__(self,i):returnself.items[i]

用法在此:

123456789101112 >>>a=SimpleList(1,2,3)>>>it=iter(a)>>>next(it)1>>>next(it)2>>>next(it)3>>>next(it)Traceback(most recent call last):File"<stdin>",line1,in<module>StopIteration

現在來看一個更有趣的例子:根據初始條件使用迭代器生成Hofstadter Q序列。Hofstadter在他的著作《Gödel, Escher, Bach: An Eternal Golden Braid》中首次提到了這個巢狀的序列,並且自那時候開始關於證明這個序列對所有n都成立的問題就開始了。以下的程式碼使用一個迭代器來生成給定n的Hofstadter序列,定義如下:

Q(n)=Q(n-Q(n-1))+Q(n−Q(n−2))

給定一個初始條件,舉個例子,qsequence([1, 1])將會生成H序列。我們使用StopIteration異常來指示序列不能夠繼續生成了,因為需要一個合法的下標索引來生成下一個元素。例如如果初始條件是[1,2],那麼序列生成將立即停止。

1234567891011121314151617 classqsequence(object):def __init__(self,s):self.s=s[:]def next(self):try:q=self.s[-self.s[-1]]+self.s[-self.s[-2]]self.s.append(q)returnqexcept IndexError:raise StopIteration()def __iter__(self):returnselfdef current_state(self):returnself.s

用法在此:

1234567 >>>Q=qsequence([1,1])>>>next(Q)2>>>next(Q)3>>>[next(Q)for__ inxrange(10)][3,4,5,5,6,6,6,8,8,8]

Generators

生成器(Generator)是一種用更簡單的函式表示式定義的生成器。說的更具體一些,在生成器內部會用到yield表示式。生成器不會使用return返回值,而當需要時使用yield表示式返回結果。Python的內在機制能夠幫助記住當前生成器的上下文,也就是當前的控制流和區域性變數的值等。每次生成器被呼叫都適用yield返回迭代過程中的下一個值。__iter__方法是預設實現的,意味著任何能夠使用迭代器的地方都能夠使用生成器。下面這個例子實現的功能同上面迭代器的例子一樣,不過程式碼更緊湊,可讀性更強。

12345 def count_generator():n=0whileTrue:yieldnn+=1

來看看用法:

12345678910111213 >>>counter=count_generator()>>>counter<generator objectcount_generator at0x106bf1aa0>>>>next(counter)0>>>next(counter)1>>>iter(counter)<generator objectcount_generator at0x106bf1aa0>>>>iter(counter)iscounterTrue>>>type(counter)<type'generator'>

現在讓我們嘗試用生成器來實現Hofstadter’s Q佇列。這個實現很簡單,不過我們卻不能實現前的類似於current_state那樣的函數了。因為據我所知,不可能在外部直接訪問生成器內部的變數狀態,因此如current_state這樣的函式就不可能實現了(雖然有諸如gi_frame.f_locals這樣的資料結構可以做到,但是這畢竟是CPython的特殊實現,並不是這門語言的標準部分,所以並不推薦使用)。如果需要訪問內部變數,一個可能的方法是通過yield返回所有的結果,我會把這個問題留作練習。

123456789 def hofstadter_generator(s):a=s[:]whileTrue:try:q=a[