理解迭代器是每個嚴(yán)肅的 Python 使用者學(xué)習(xí) Python 道路上的里程碑,。本文將從零開始,,一步一步帶你認(rèn)識 Python 中基于類的迭代器,。 相較于其他編程語言,,Python 的語法是優(yōu)美而清晰的。比如 for-in 循環(huán)這種極具 Python 特色的代碼,,讓你讀起來感覺就像讀一個英語句子一樣,。它很能體現(xiàn) Python 的美感。 numbers = [1, 2, 3] for n in numbers: print(n) 但是你知道這種優(yōu)雅的循環(huán)結(jié)構(gòu)背后是如何工作的嗎,?循環(huán)是如何從它所循環(huán)的那個對象中獲取獨(dú)立元素的,?你怎么才能在自己創(chuàng)建的 Python 對象上使用相同的編程風(fēng)格呢? Python 迭代器協(xié)議為上邊這些疑問提供了答案:
支持 __iter__ 和 __next__ 魔法方法的對象可自動適用于 for-in 循環(huán)。 就像 Python 中的裝飾器一樣,,迭代器及其相關(guān)技術(shù)乍看起來相當(dāng)神秘而復(fù)雜,。不用擔(dān)心,讓我們一步一步慢慢來認(rèn)識它,。 我們先聚焦于 Python 3 中迭代器的核心機(jī)制,,去除不必要的麻煩枝節(jié),這樣就可以在基礎(chǔ)層面清楚地看到迭代器是如何運(yùn)作的,。 下文中,,我們會編寫幾個支持迭代器協(xié)議的 Python 類。每個例子都和上邊提到的幾個 for-in 循環(huán)問題相關(guān)聯(lián),。 好,,我們現(xiàn)在進(jìn)入正題! 【永久迭代的 Python 迭代器】 我們來編寫一個可以演示 Python 迭代器協(xié)議輪廓的類,。你可能學(xué)習(xí)過其他迭代器教程,,但這里使用的例子和你在那些教程中看到的例子有些不同,或許會更有助于你的理解,。 我們將會實(shí)現(xiàn)一個名為 Repeater 的類,,這個類的對象可使用 for-in 循環(huán)來迭代。 repeater = Repeater('Hello') for item in repeater: print(item) (代碼片段1) 誠如類的名字,,當(dāng)被迭代時,,該類的對象實(shí)例會重復(fù)返回一個單一的值。上邊這段示例代碼會不停地在控制臺窗口中打印 Hello 字符串,。 我們先來定義這個 Repeater 類: class Repeater: def __init__(self, value): self.value = value
def __iter__(self): return RepeaterIterator(self) 這個類看起來和普通的 Python 類沒什么區(qū)別,。但是請注意它包含了一個 __iter__ 魔法方法。這個方法返回了一個 RepeaterIterator 對象,。 這個 RepeaterIterator 對象是什么,?它是一個我們需要定義的助手類,借助于 RepeaterIterator,,代碼片段一中的 repeater 對象才能在 for-in 循環(huán)中運(yùn)行起來,。 RepeaterIterator 類的定義如下: class RepeaterIterator: def __init__(self, source): self.source = source
def __next__(self): return self.source.value 同樣,它看起來也是一個簡單的 Python 類,。不過你需要留意以下兩點(diǎn):
Repeater 和 RepeaterIterator 兩個類共同實(shí)現(xiàn)了 Python 的迭代器協(xié)議,。其中的關(guān)鍵是 __iter__ 和 __next__ 這兩個魔法方法,正是它們才使得一個 Python 對象成為一個可迭代對象(iterable)。 有了這兩個類的定義,,你現(xiàn)在可以運(yùn)行代碼片段一了,。結(jié)果就是,屏幕上不停地打印 Hello 字符串: Hello Hello Hello Hello Hello ... 恭喜,!你已經(jīng)實(shí)現(xiàn)了一個可以工作的 Python 迭代器,,并在 for-in 循環(huán)中使用了它。 接下來,,我們將拆解這個例子,,從而更好地理解 __iter__ 和 __next__ 是如何共同使得一個 Python 對象成為可迭代的。 【for-in 循環(huán)是如何工作的】 我們看到,,for-in 循環(huán)可以從 Repeater 對象中獲取新元素,,那么,for-in 是如何與 Repeater 對象通信的呢,? 我們對代碼片段一做些展開,,并保持同樣的運(yùn)行結(jié)果: repeater = Repeater('Hello') iterator = repeater.__iter__() while True: item = iterator.__next__() print(item) 如你所見,for-in 就是一個簡單 while 循環(huán)的語法糖,。
如果你從事過數(shù)據(jù)庫應(yīng)用開發(fā),使用過數(shù)據(jù)庫游標(biāo)(cursor),,那對此模型應(yīng)該就不陌生了:先初始化一個游標(biāo),,將其準(zhǔn)備(prepare)好用來讀取數(shù)據(jù),然后從中獲取數(shù)據(jù)(fetch)并保存到本地變量中,。 由于只占用了一個 value 的空間,,這種方式具備很高的內(nèi)存效率。Repeater 類可以用作一個包含元素的無限序列,,而這無法通過 Python 列表來實(shí)現(xiàn),,我們不可能創(chuàng)建一個包含無限多元素的 list 對象。這是迭代器的一個強(qiáng)大的功能,。 使用迭代器的另一個好處是,,它提供了一種抽象語義。迭代器可用于多種容器,,它為這些容器提供了統(tǒng)一的接口,,你不需要關(guān)心容器的內(nèi)部結(jié)構(gòu)就可以從中提取每個元素。 無論你使用列表,、字典,、無限序列還是別的序列類型,,它們都只是代表了一種實(shí)現(xiàn)上的細(xì)節(jié),。而你可以通過迭代器以相同的方式來遍歷它們。 我們看到,for-in 循環(huán)并無特別之處,,它只是在正確的時間調(diào)用了正確的魔法方法而已,。 實(shí)際上,我們也可以在 Python 解釋器命令窗口中手動模擬循環(huán)是如何使用迭代器協(xié)議的,。 >>> repeater = Repeater('Hello') >>> iterator = iter(repeater) >>> next(iterator) 'Hello' >>> next(iterator) 'Hello' >>> next(iterator) 'Hello' ... 每調(diào)用一次 next(),,iterator 就會輸出一個 Hello。 這里,,我們使用 Python 內(nèi)置的 iter() 和 next() 函數(shù)來代替對象的 __iter__ 和 __next__ 方法,。這些內(nèi)置函數(shù)其實(shí)也是調(diào)用了對應(yīng)的魔法函數(shù),只不過它們?yōu)榈鲄f(xié)議披上了一件干凈的外衣,,使代碼看起來更漂亮更簡單,。 通常,使用內(nèi)置函數(shù)比直接訪問魔法方法要好,,因?yàn)榇a的可讀性更強(qiáng)一些,。 【一個更簡單的迭代器類】 上邊的例子中,我們使用兩個獨(dú)立的類來實(shí)現(xiàn)迭代器協(xié)議,,每個類承擔(dān)協(xié)議的一部分職責(zé),。而很多時候,這兩項(xiàng)職責(zé)可以由一個類來承擔(dān),,從而減少代碼量,。 我們現(xiàn)在就來簡化之前實(shí)現(xiàn)迭代器協(xié)議的方法。 還記得我們?yōu)槭裁匆x RepeaterIterator 這個類嗎,?我們用它來定義獲取元素的 __next__ 方法,。而實(shí)際上,在什么地方定義 __next__ 方法是無關(guān)緊要的,。 迭代器協(xié)議關(guān)心的是,,__iter__ 方法必須返回一個提供了 __next__ 方法的對象。 我們再審視一下 RepeaterIterator 這個類,,它其實(shí)并沒有做太多的工作,,僅僅返回了 source 的成員變量 value。我們是不是可以省去這個類,,將其功能融入 Repeater 類中,?可以試一下。 我們可以這樣來實(shí)現(xiàn)新的并且更簡單的迭代器類,。 class Repeater: def __init__(self, value): self.value = value
def __iter__(self): return self
def __next__(self): return self.value 解釋一下:
使用代碼片段一來測試這個 Repeater 類,,同樣會不停地輸出 Hello,。 通過兩個類來實(shí)現(xiàn)迭代器協(xié)議,我們能很好地理解迭代器協(xié)議的底層原則,;而使用一個類則可以提升開發(fā)效率,,非常有意義。 【迭代器不能永遠(yuǎn)迭代下去】 我們已經(jīng)理解了迭代器的工作原理,,但是我們目前實(shí)現(xiàn)的迭代器僅僅可以無限迭代下去,,而這并非 Python 中迭代器的主要使用場景。 在本文的開頭,,我們舉了個簡單 for-in 循環(huán)的例子作為引子: numbers = [1, 2, 3] for n in numbers: print(n) 這才是更加普遍的應(yīng)用場景:輸出有限的元素,,然后終止。 我們該如何實(shí)現(xiàn)這樣的迭代器呢,?迭代器如何給出元素耗盡迭代結(jié)束的信號呢,? 或許你會想到,可以在迭代結(jié)束時返回 None,。聽起來是個不錯的主意,,但是并非適合所有情況,某些場景可能不接受 None 作為合法值,。 我們可以看一下 Python 中的其他迭代器是如何處理這一問題的,。我們先創(chuàng)建一個簡單的容器:一個包含若干元素的 list。然后迭代這個 list,,直到元素耗盡,,看看會發(fā)生什么情況。 >>> my_list = [1, 2, 3] >>> iterator = iter(my_list)
>>> next(iterator) 1 >>> next(iterator) 2 >>> next(iterator) 3 注意,,此時我們已消費(fèi)了 list 中的所有元素,。如果繼續(xù)調(diào)用 next(),會發(fā)生什么,? >>> next(iterator) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration 報異常了,! 沒錯:迭代器就是使用異常來改變控制流程的。為了提示迭代的結(jié)束,,Python 迭代器簡單地拋出一個內(nèi)置的 StopIteration 異常,。 Python 迭代器正常情況下不能被重置。一旦耗盡,,每次在迭代器上調(diào)用 next() ,,迭代器都會拋出 StopIteration 異常。如果想重新執(zhí)行迭代,,你需要通過 iter() 函數(shù)來獲取一個全新的迭代器對象,。 現(xiàn)在我們已經(jīng)弄清楚了如何編寫一個非無限迭代的迭代器類了,。我們將其命名為 BoundedRepeater,,它可以在執(zhí)行有限次重復(fù)后停止迭代,。 class BoundedRepeater: def __init__(self, value, max_repeats): self.value = value self.max_repeats = max_repeats self.count = 0
def __iter__(self): return self
def __next__(self): if self.count >= self.max_repeats: raise StopIteration self.count += 1 return self.value BoundedRepeater 可以為我們提供想要的結(jié)果。當(dāng)?shù)螖?shù)超過 max_repeats 指定的值時,,迭代就會停止,。 >>> repeater = BoundedRepeater('Hello', 3) >>> for item in repeater: print(item) Hello Hello Hello 如果我們移除上邊這個 for-in 循環(huán)中的語法糖,它可以重寫為: repeater = BoundedRepeater('Hello', 3) iterator = iter(repeater) while True: try: item = next(iterator) except StopIteration: break print(item) 我們需要在每次調(diào)用 next() 時手動檢查是否有異常拋出,。for-in 循環(huán)替我們做了這項(xiàng)工作,,讓代碼變得更易讀和更易維護(hù)。這也是 Python 迭代器為何如此強(qiáng)大的另一個原因,。 【兼容 Python 2.x 的迭代器】 上文中用到的例子都是使用 Python 3 編寫的,。如果你使用 Python 2 來實(shí)現(xiàn)基于類的迭代器,需要注意一個細(xì)小但重要的區(qū)別:
這個命名上的區(qū)別可能會導(dǎo)致代碼的不兼容。 解決辦法也很簡單,,讓迭代器類同時支持這兩個方法,。 以那個無限序列類為例: class InfiniteRepeater(object): def __init__(self, value): self.value = value
def __iter__(self): return self
def __next__(self): return self.value
# Python 2 compatibility: def next(self): return self.__next__() 我們?yōu)槠湓黾恿艘粋€ next() 方法,該方法僅僅調(diào)用 __next__() 就可以了,。 還有一個變動,,我們將類的定義修改為繼承自 object 類,,以使其符合 Python 2 中的新式類定義方法。這和迭代器無關(guān),,只是一個良好的習(xí)慣,。 【結(jié)語】
【近期熱門文章】 |
|