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

熊崽Kevin發表於2014-04-29

【譯註】:作為一門動態指令碼語言,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卻沒有。

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

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

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

用法在此:

現在來看一個更有趣的例子:根據初始條件使用迭代器生成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],那麼序列生成將立即停止。

用法在此:

Generators

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

來看看用法:

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

請注意,在生成器迭代過程的結尾有一個簡單的return語句,但並沒有返回任何資料。從內部來說,這將丟擲一個StopIteration異常。

下一個例子來自Groupon的面試題。在這裡我們首先使用兩個生成器來實現Bernoulli過程,這個過程是一個隨機布林值的無限序列,True的概率是p而False的概率為q=1-p。隨後實現一個von Neumann extractor,它從Bernoulli process獲取輸入(0<p<1),並且返回另一個Bernoulli process(p=0.5)。

最後,生成器是一種生成隨機動態系統的很有利的工具。下面這個例子將演示著名的帳篷對映(tent map)動態系統是如何通過生成器實現的。(插句題外話,看看數值的不準確性是如何開始關聯變化並呈指數式增長的,這是一個如帳篷對映這樣的動態系統的關鍵特徵)。

另一個相似的例子是Collatz序列。

請注意在這個例子中,我們仍舊沒有手動丟擲StopIteration異常,因為它會在控制流到達函式結尾的時候自動丟擲。

請看用法:

Recursive Generators

生成器可以像其它函式那樣遞迴。讓我們來看一個自實現的簡單版本的itertools.permutations,這個生成器通過給定一個item列表生成其全排列(在實際中請使用itertools.permutations,那個實現更快)。基本思想很簡單:對列表中的每一個元素,我們通過將它與列表第一個元素交換將其放置到第一的位置上去,而後重新遞迴排列列表的剩餘部分。

Generator Expressions

生成器表示式可以讓你通過一個簡單的,單行宣告定義生成器。這跟Python中的列表推導式非常類似,舉個例子,下面的程式碼將定義一個生成器迭代所有的完全平方。注意生成器表示式的返回結果是一個生成器型別物件,它實現了next和__iter__兩個方法。

同樣可以使用生成器表示式實現Bernoulli過程,在這個例子中p=0.4。如果一個生成器表示式需要另一個迭代器作為迴圈指示器,並且這個生辰器表示式使用在無限序列上的,那麼itertools.count將是一個很好的選擇。若非如此,xrange將是一個不錯的選擇。

正如前面提到的,生成器表示式能夠用在任何需要迭代器作為引數的地方。舉個例子,我們可以通過如下程式碼計算前十個全平方數的累加和:

更多生成器表示式的例子將在下一節給出。

itertools模組

itertools模組提供了一系列迭代器能夠幫助使用者輕鬆地使用排列、組合、笛卡爾積或其他組合結構。

在開始下面的部分之前,注意到上面給出的所有程式碼都是未經優化的,在這裡只是充當一個示例的作用。在實際使用中,你應該避免自己去實現排列組合除非你能夠有更好的想法,因為列舉的數量可是按照指數級增加的。

讓我們先從一些有趣的用例開始。第一個例子來看如何寫一個常用的模式:迴圈遍歷一個三維陣列的所有下標元素,並且迴圈遍歷滿足0≤i<j<k≤n條件的所有下標。

使用itertools模組提供的列舉器有兩個好處:程式碼能夠在單行內完成,並且很容易擴充套件到更高維度。我並未比較for方法和itertools兩種方法的效能,也許跟n有很大關係。如果你想的話請自行測試評判。

第二個例子,來做一些有趣的數學題:使用生成器表示式、itertools.combinations以及itertools.permutations來計算排列的逆序數,並且計算一個列表全排列逆序數之和。如OEIS A001809所示,求和的結果趨近於n!n(n-1)/4。在實際使用中直接通過這公式計算要比上面的程式碼更高效,不過我寫這個例子是為了練習itertools列舉器的使用。

用法如下:

第三個例子,通過brute-force counting方法計算recontres number。recontres number的定義在此。首先,我們寫了一個函式在一個求和過程中使用生成器表示式去計算排列中fixed points出現的個數。然後在求和中使用itertools.permutations和其他生成器表示式計算包含n個數並且有k個fixed points的排列的總數。然後得到結果。當然了,這個實現方法是效率低下的,不提倡在實際應用中使用。再次重申,這只是為了掩飾生成器表示式以及itertools相關函式使用方法的示例。

用法:

擴充套件閱讀

http://linuxgazette.net/100/pramode.html

http://www.dabeaz.com/generators/

致謝

謝謝reddit使用者jms_nh對本文的修改建議。

相關文章