奇妙的流控制 Python中的迭代器與生成器
在Python 2.2中引進(jìn)了一種帶有新關(guān)鍵字的新型構(gòu)造。這種構(gòu)造是生成器;關(guān)鍵字是yield。生成器使幾個新型、強(qiáng)大和富有表現(xiàn)力的編程習(xí)慣用法成為可能,但初看,要理解生成器,還是有一點困難。
51CTO推薦閱讀:深入了解Python暫緩列表生成器
由于迭代器比較容易理解,讓我們先來看它?;旧希?迭代器是含有 .next() 方法的對象。唔,這樣定義不十分正確,但非常接近。事實上,當(dāng)?shù)鲬?yīng)用新的 iter() 內(nèi)置函數(shù)時,大多數(shù)迭代器的上下文希望得到一個可以生成迭代器的對象。為使用戶定義的類(該類含有必不可少的 .next() 方法)返回迭代器,需要使 __iter__() 方法返回 self 。本文中的示例會清楚地說明這一點。如果迭代有一個邏輯終止,則迭代器的 .next() 方法可能決定拋出 StopIteration 異常。
生成器要稍微復(fù)雜和一般化一點。但生成器最典型的用途是用來定義迭代器;所以不值得總是為一些細(xì)微之處而擔(dān)心。 生成器是這樣一個函數(shù),它記住上一次返回時在函數(shù)體中的位置。對生成器函數(shù)的第二次(或第 n 次)調(diào)用跳轉(zhuǎn)至該函數(shù)中間,而上次調(diào)用的所有局部變量都保持不變。
在某些方面,生成器就象本專欄前面文章討論的函數(shù)型編程中的“終止”。象“終止”一樣,生成器“記住”了它數(shù)據(jù)狀態(tài)。但生成器比“終止”要更進(jìn)一步:生成器還“記住”了它在流控制構(gòu)造(在命令式編程中,這種構(gòu)造不只是數(shù)據(jù)值)中的位置。由于連續(xù)性使您在執(zhí)行框架間任意跳轉(zhuǎn),而不總是返回到直接調(diào)用者的上下文(如同生成器那樣),因此它仍是比較一般的。幸運的是,使用生成器比理解程序流和狀態(tài)的所有概念性問題容易得多。實際上,稍加實踐之后,就可以象普通函數(shù)那樣容易地使用生成器。
隨機(jī)遍歷
讓我們考慮一個相當(dāng)簡單的問題,可以用多種方法來解決它 ― 新方法和舊方法都可以。假設(shè)我們想要一串正的隨機(jī)數(shù)字流,它比服從向后參考約束的數(shù)字流要小。明確的講,我們希望每個后續(xù)數(shù)字比前一個數(shù)字至少大或小 0.4。而且,數(shù)字流本身不是無限的,在幾個隨機(jī)步驟后結(jié)束。這個示例中,當(dāng)數(shù)字流中產(chǎn)生小于 0.1 的數(shù)字時,我們將簡單地結(jié)束它。上述的約束有點象可以在“隨機(jī)遍歷”算法找到的約束,結(jié)束條件類似“統(tǒng)計”或“局部最小值”結(jié)果 ― 但當(dāng)然,這要比大多數(shù)現(xiàn)實世界中簡單。在 Python 2.1或更早的版本中,我們有幾種方法來解決這個問題。一種方法是,簡單地生成流中的數(shù)字列表并返回它??赡芸雌饋硐螅?/p>
- RandomWalk_List.py
- import
- random
- def
- randomwalk_list
- ():
- last, rand = 1, random.random()
- # init candidate elements
- nums = []
- # empty list
- while
- rand > 0.1:
- # threshhold terminator
- if
- abs(last-rand) >= 0.4:
- # accept the number
- last = rand
- nums.append(rand)
- # add latest candidate to nums
- else
- :
- '*',
- # display the rejection
- rand = random.random()
- # new candidate
- nums.append(rand)
- # add the final small element
- return
- nums
利用這個函數(shù)就象如下所示般簡單:
- 隨機(jī)遍歷列表的迭代
- for num in randomwalk_list():
- print num,
上面這種方法中有幾個值得注意的局限性。這個特定的示例中極不可能產(chǎn)生龐大的數(shù)字列表,但只通過將閥值終結(jié)符定義得較嚴(yán)格,就可以創(chuàng)建任意大流(隨機(jī)精確大小,但可以預(yù)見數(shù)量級)。在某種程度上,內(nèi)存和性能問題可能使得這種方法不切實際,以及沒有必要。同樣是這個問題,使得 Python 較早的版本中添加了 xrange() 和 xreadlines() 。更重要的是,許多流取決于外部事件,并且當(dāng)每個元素可用時,才處理這些流。例如,流可以偵聽一個端口,或者等待用戶輸入。試圖在流之外創(chuàng)建完整的列表并不就是這些情形中的某一種。
在 Python 2.1 和較早版本中,我們的訣竅是使用“靜態(tài)”函數(shù)局部變量來記住關(guān)于函數(shù)的上一次調(diào)用的一些事情。顯而易見,全局變量可以做同樣的工作,但它們帶來了大家熟知的全局性名稱空間污染的問題,并會因非局部性而引起錯誤。這里,如果您不熟悉這個訣竅,可能會感到詫異 ― Python 沒有“正式”的靜態(tài)范圍聲明。然而,如果賦予了命名參數(shù)可變的缺省值,那么參數(shù)就可以,用作以前調(diào)用的持久存儲器。明確的講,列表是一些便利的可變對象,他們甚至可以方便地保留多個值。使用“靜態(tài)”方法,可以編寫如下的函數(shù):
- RandomWalk_Static.py
- import
- random
- def
- randomwalk_static
- (last=[1]):
- # init the "static" var(s)
- rand = random.random()
- # init a candidate value
- if
- last[0] < 0.1:
- # threshhold terminator
- return
- None
- # end-of-stream flag
- while
- abs(last[0]-rand) < 0.4:
- # look for usable candidate
- '*',
- # display the rejection
- rand = random.random()
- # new candidate
- last[0] = rand
- # update the "static" var
- return
- rand
這個函數(shù)是十分友好的存儲器。它只需要記住一個以前的值,返回一個單個數(shù)字(不是一個數(shù)字的大列表)。并且與此類似的一個函數(shù)可以返回取決于(部分地或完全地)外部事件的連續(xù)的值。不利的一面是,利用這個函數(shù)有點不夠簡練,且相當(dāng)不靈活。
- 靜態(tài)隨機(jī)遍歷的迭代
- num = randomwalk_static()
- while num is not None:
- print num,
- num = randomwalk_static()
#p#
新的遍歷方法
實質(zhì)上,Python 2.2 序列都是迭代器。Python 常見的習(xí)慣用法 for elem in lst: 現(xiàn)在實際上讓 lst 產(chǎn)生一個迭代器。然后, for 循環(huán)反復(fù)調(diào)用這個迭代器的 .next() 方法,直到它遇到 StopIteration 異常為止。幸運的是,由于所有常見的內(nèi)置類型自動產(chǎn)生它們的迭代器,所以 Python 程序員不需要知道這里發(fā)生了什么。實際上,現(xiàn)在字典里有 .iterkeys() 、 .iteritems() 和 .itervalues() 方法來產(chǎn)生迭代器;首要的是在新的習(xí)慣用法 for key in dct: 中使用了什么。同樣,通過調(diào)用 .readline() 迭代器支持新的習(xí)慣用法 for line in file: 。
但是如果實際所產(chǎn)生的是在 Python 解釋器內(nèi),則顯而易見要用定制類來產(chǎn)生它們自己的迭代器,而不是專使用內(nèi)置類型的迭代器。定制類支持直接使用 randomwalk_list() 以及一次一個元素這種“極度節(jié)省”的 randomwalk_static ,它是簡單易懂的:
- RandomWalk_Iter.py
- import
- random
- class
- randomwalk_iter
- :
- def
- __init__
- (self):
- self.last = 1
- # init the prior value
- self.rand = random.random()
- # init a candidate value
- def
- __iter__
- (self):
- return
- self
- # simplest iterator creation
- def
- next
- (self):
- if
- self.rand < 0.1:
- # threshhold terminator
- raise
- StopIteration
- # end of iteration
- else
- :
- # look for usable candidate
- while
- abs(self.last-self.rand) < 0.4:
- '*',
- # display the rejection
- self.rand = random.random()
- # new candidate
- selfself.last = self.rand
- # update prior value
- return
- self.rand
這個定制迭代器看起來確實如同由函數(shù)生成的真實列表一樣:
- 隨機(jī)遍歷類的迭代
- for num in randomwalk_iter():
- print num,
事實上,即使支持習(xí)慣用法 if elem in iterator ,它僅嘗試為確定真值所需要的那么多的迭代器的元素,(如果最終的值為 false,當(dāng)然,它就需要測試所有元素)。
#p#
美中不足
上述方法對于手邊的問題非常好用。但沒有一種方法能很好地解決這樣的情形:例程在運行中創(chuàng)建了大量的局部變量,并把它的運行簡化為循環(huán)和條件的嵌套。如果帶靜態(tài)(或全局)變量的迭代器類或函數(shù)取決于多個數(shù)據(jù)狀態(tài),則出現(xiàn)兩個問題。一個是一般性問題:創(chuàng)建多個實例屬性或靜態(tài)列表元素來保留每個數(shù)據(jù)值。更為重要的問題是計算如何確切地返回到與數(shù)據(jù)狀態(tài)相符的流邏輯的相關(guān)部分。非常容易忘記不同數(shù)據(jù)間的相互作用和互相依存。
生成器完全繞過了整個問題。生成器“返回”時帶關(guān)鍵字 yield ,但“記住”了它“返回”的所有確切執(zhí)行位置。下次調(diào)用生成器時,它再接著上次的位置 — 包括函數(shù)流和變量值這兩個方面。
在 Python 2.2+ 中,不直接 寫生成器。相反,編寫一個函數(shù),當(dāng)調(diào)用它時,返回生成器。這可能看起來有點古怪,但“函數(shù)工廠”是 Python 的常見特性,并且“生成器工廠”明顯是這個概念性擴(kuò)展。在 Python 2.2+ 中使函數(shù)成為生成器工廠是它主體某處的一個或多個 yield 語句。如果 yield 發(fā)生, return 一定只發(fā)生在沒有伴隨任何返回值的情況中。然而,一個較好的選擇是,安排函數(shù)體以便于完成所有 yield 之后,執(zhí)行就“跳轉(zhuǎn)到結(jié)束”。但如果遇到 return ,它導(dǎo)致產(chǎn)生的生成器拋出 StopIteration 異常,而不是進(jìn)一步生成值。
從我的觀點來看,過去對生成器工廠的語法選擇有點欠缺。 yield 語句可以非常好地存在于函數(shù)體中,您可能無法確定是否函數(shù)一定會在函數(shù)體最初 N 行內(nèi)的某處作為生成器工廠而存在。當(dāng)然,對于函數(shù)工廠,也存在這樣的問題,但是由于函數(shù)工廠不改變函數(shù)體的實際 語法(并且有時允許函數(shù)體返回普通值,盡管這可能不是出自良好的設(shè)計)。對于我來說,新關(guān)鍵字 ― 比如 generator 代替 def ― 會是一個比較好的選擇。
先不考慮語法,當(dāng)調(diào)用生成器來擔(dān)當(dāng)?shù)鲿r,生成器有良好的狀況來自動擔(dān)當(dāng)?shù)?。這里不需要象類的 .__iter__() 方法。遇到的每個 yield 都成為生成器的 .next() 方法的返回值。為了清楚起見,我們來看一個最簡單的生成器:
- 最簡單可行的 Python 2.2 生成器
- >>>
- from
- __future__
- import
- generators
- >>>
- def
- gen
- ():
- yield 1
- >>> g = gen()
- >>> g.next()
- 1
- >>> g.next()
- Traceback (most recent call last):
- File "<pyshell#15>", line 1,
- in
- ?
- g.next()
- StopIteration
讓我們使生成器工作在我們樣本問題中:
- RandomWalk_Generator.py
- from
- __future__
- import
- generators
- # only needed for Python 2.2
- import
- random
- def
- randomwalk_generator
- ():
- last, rand = 1, random.random()
- # initialize candidate elements
- while
- rand > 0.1:
- # threshhold terminator
- '*',
- # display the rejection
- if
- abs(last-rand) >= 0.4:
- # accept the number
- last = rand
- # update prior value
- yield rand
- # return AT THIS POINT
- rand = random.random()
- # new candidate
- yield rand
- # return the final small element
這個定義的簡單性是吸引人的??梢允止せ蛘咦鳛榈鱽砝眠@個生成器。在手工情形下,生成器可以在程序中傳遞,并且無論在哪里以及無論何時需要(這非常靈活),都可以調(diào)用。手工情形的一個簡單示例是:
- 隨機(jī)遍歷生成器的手工使用
- gen = randomwalk_generator()
- try:
- while 1: print gen.next(),
- except StopIteration:
- pass
然而,更多情況下,可能將生成器作為迭代器來使用,這樣更為簡練(并且看起來又象只是一個老式的序列):
- 作為迭代器的隨機(jī)遍歷生成器
- for num in randomwalk_generator():
- print_short(num)
結(jié)束語
Python 程序員需要花一點時間來熟悉生成器的來龍去脈。最初這樣一個簡單構(gòu)造所增加的能力是令人驚奇的;并且我預(yù)言,甚至熟練的程序員(象 Python 開發(fā)人員自己)也需要花一些時間來繼續(xù)發(fā)現(xiàn)使用生成器過程中的一些微妙的新技術(shù)。
【編輯推薦】