花下貓語:以前說過,我對於編程語言跟其它學科的融合很是感興趣,但我還說漏了一點,就是我對於 Python 跟其它編程語言的對比學習,也很感興趣。因此,我一直但願能彙集一些有其它語言基礎的同窗,一塊兒討論共通的語言特性間的話題。不一樣語言的碰撞,經常能帶給人更高維的視角,也能觸及到語言的根基,這個過程是極有益的。python
這篇文章是羣內 櫻雨樓 小姐姐的投稿,她是咱們學習羣裏的真·大佬,說到對 Python 的研究以及高階知識的水平,無人能出其右(羣裏不少同窗都被她實力圈粉啦)。除了 Python,她對 C++、Perl、Go 與 Fortran 等語言都有涉獵,本文主要是對比了 Python 與 C++,來深刻談談迭代器。話很少說,請看正文。編程
櫻雨樓 | 原創做者設計模式
豌豆花下貓 | 編輯潤色數組
本文原創並首發於公衆號【Python貓】,未經受權,請勿轉載。安全
原文地址:https://mp.weixin.qq.com/s/Be...數據結構
迭代器(Iterator)是 Python 以及其餘各類編程語言中的一個很是常見且重要,但又充滿着神祕感的概念。不管是 Python 的基礎內置函數,仍是各種高級話題,都到處可見迭代器的身影。app
那麼,迭代器到底是怎樣的一個概念?其又爲何會普遍存在於各類編程語言中?本文將基於 C++ 與 Python,深刻討論這一系列問題。dom
什麼是迭代器?當我初學 Python 的時候,我將迭代器理解爲一種可以放在「for xxx in ...」的「...」位置的東西;後來隨着學習的深刻,我瞭解到迭代器就是一種實現了迭代器協議的對象;學習 C++ 時,我瞭解到迭代器是一種行爲和指針相似的對象...編程語言
事實上,迭代器是一個伴隨着迭代器模式(Iterator Pattern)而生的抽象概念,其目的是分離並統一不一樣的數據結構訪問其中數據的方式,從而使得各類須要訪問數據結構的函數,對於不一樣的數據結構能夠保持相同的接口。函數
在不少討論 Python 迭代器的書籍與文章中,我看到這樣兩種觀點:1. 迭代器是爲了節約數據結構所產生的內存;2. 遍歷迭代器效率更高。
這兩點論斷都是很不許確的:首先,除了某些不定義在數據結構上的迭代器(如文件句柄,itertools 模塊的 count、cycle 等無限迭代器等),其餘迭代器都定義在某種數據結構上,因此不存在節約內存的優點;其次,因爲迭代器是一種高度泛化的實現,其須要在每一次迭代器移動時都作一些額外工做(如 Python 須要不斷檢測迭代器是否耗盡,並進行異常監測;C++ 的 deque 容器須要對其在堆上用於存儲的多段不連續內存進行銜接等),故遍歷迭代器的效率必定低於或幾乎接近於直接遍歷容器,而不太可能高於直接遍歷原容器。
綜上所述,迭代器存在的意義,不是爲了空間換時間,也不是爲了時間換空間,而是一種適配器(Adapter)。迭代器的存在,使得咱們可使用一樣的 for 語句去遍歷各類容器,或是像 C++ 的 algorithm 模塊所示的那樣,使用一樣的接口去處理各類容器。
這些容器能夠是一個連續內存的數組或列表,或是一個多段連續內存的 deque,甚至是一個徹底不連續內存的鏈表或是哈希表等等,咱們徹底不須要關注迭代器對於不一樣的容器到底是怎麼取得數據的。
在 C++ 中,迭代器經過泛化指針(Generalized Pointer)的形式呈現。泛化指針與仿函數(Functor)的定義相似,其包含如下兩種狀況:
根據泛化指針爲了將其「假裝」成一個真正的指針從而重載的運算符的數量,迭代器被分爲五種,以下文所示。
C++ 中,迭代器按照其所支持的行爲被分爲五類:
對於前向迭代器,雙向迭代器,以及隨機訪問迭代器,若是其不存在底層 const(Low-Level Const)限定,則同時也支持一切輸出迭代器操做。
C++ 中還存在一系列迭代器適配器,用於使得一些非迭代器對象的行爲相似於迭代器,或修改迭代器的一些默認行爲,大體包含以下幾個類別:
在 Python 中,迭代器基於鴨子類型(Duck Type)下的迭代器協議(Iterator Protocol)實現。迭代器協議規定:若是一個類想要成爲可迭代對象(Iterable Object),則其必須實現__iter__方法,且其返回值須要是一個實現了__next__方法的對象。即:實現了__iter__方法的類將成爲可迭代對象,而實現了__next__方法的類將成爲迭代器。
顯然,__iter__方法是iter函數所對應的魔法方法,__next__方法是 next 函數所對應的魔法方法。
對於一個可迭代對象,針對「誰實現了__next__方法?」這一問題進行討論,可將可迭代對象的實現分爲兩種狀況:
class SampleIterator: def __iter__(self): return iter(...)
class SampleIterator: def __iter__(self): return self def __next__(self): # Not The End if ...: return ... # Reach The End else: raise StopIteration
此示例中能夠看出,當迭代器終止時,經過拋出 StopIteration 異常告知 Python 迭代器已耗盡。
生成器(Generator)是 Python 特有的一組特殊語法,其主要目的爲提供一個基於函數而不是類的迭代器定義方式。同時,Python 也具備生成器推導式,其基於推導式語法快速創建迭代器。生成器通常適用於須要建立簡單邏輯的迭代器的場合。
只要一個函數的定義中出現了 yield 關鍵詞,則此函數將再也不是一個函數,而成爲一個「生成器構造函數」,調用此構造函數便可產生一個生成器對象。
因而可知,若是僅討論該語法自己,而不關心實現的話:生成器只是「借用」了函數定義的語法,實際上與函數並沒有關係(並不表明生成器的底層實現也與函數無關)。示例代碼以下:
def SampleGenerator(): yield ... yield ... yield ...
生成器推導式則更爲簡單,只須要將列表推導式的中括號換爲小括號便可:
(... for ... in ...)
綜上所述,生成器是 Python 獨有的一類迭代器的特殊構造方式。生成器一旦被構造,其會自動實現完整的迭代器協議。
itertools 模塊中實現了三個特殊的無限迭代器(Infinite Iterator):count,cycle 以及 repeat,其有別於普通的表示範圍的迭代器。若是對無限迭代器進行迭代將致使無限循環,故無限迭代器一般只可以使用 next 函數進行取值。
關於無限迭代器的詳細內容,可參閱 Python 文檔。(注:舊文 Python進階:設計模式之迭代器模式 也介紹過)
通過上文的討論能夠發現,Python 只有一種迭代器,此種迭代器只能進行單向,單步前進操做,且不可做爲左值。故 Python 的迭代器在 C++ 中應屬於單向只讀迭代器,這是一種很低級的迭代器。
此外,因爲迭代器只支持單向移動,故一旦向前移動便不可回頭,若是遍歷一個已耗盡迭代器,則 for 循環將直接退出,且無任何錯誤產生,此種行爲每每會產生一些難以察覺的 bug,實際使用時請務必注意。
綜上所述,Python 對於迭代器的實現實際上是高度匱乏的,應謹慎使用。
因爲迭代器自己並非獨立的數據結構,而是指向其餘數據結構中的值的泛化指針,故和普通指針同樣,一旦指針指向的內存發生變更,則迭代器也將隨之失效。
若是迭代器指向的數據結構是隻讀的,則顯然,直到析構函數被調用,迭代器都不會失效。但若是迭代器所指向的數據結構在其存在時發生了插入或刪除操做,則迭代器將可能失效。故討論某個操做是否會致使指向容器的迭代器失效,是一個很重要的話題。
因爲 Python 中沒有 C++ 的 list、deque 等數據結構實現,故本文只簡單地討論 vector 與 unordered_map 這兩種數據結構的迭代器有效性。
對於 vector,因爲其存在內存擴容與轉移操做,故任何會潛在致使內存擴容的方法都將損壞迭代器,包括 push_back、emplace_back、insert、emplace 等。
unordered_map 與 vector 的情形相似,對 unordered_map 進行任何插入操做也將損壞迭代器。
注:本節所討論所有內容均基於實際行爲進行猜測和推論,並無通過對 Python 源代碼的考察和驗證,僅供讀者參考。
考察以下代碼:
numList = [1, 2, 3] numListIter = iter(numList) next(numListIter) for i in range(1000000): numList.append(i) # print 2 print(next(numListIter))
若是在 C++ 中對一個 vector 執行這麼屢次的 push_back,則指向第二個元素的迭代器必定早已失效。但在 Python 中能夠看到,指向 List 的迭代器並未失效,其仍然返回了 2。
故可猜測:Python 對於 List 所產生的迭代器並不跟蹤指向 List 元素的指針,而僅僅跟蹤的是容器的索引值。
numList = [1,2] numListIter = iter(numList) # 1 next(numList) numList.append(3) # 2 next(numListIter) # 3 print(next(numListIter))
首先,Python 不存在尾迭代器這一律念。但由上述代碼可知,當迭代器所指向的 List 變長後,迭代器的終止點也隨之變化,即:原先的尾迭代器將再也不適用。
按照「迭代器僅跟蹤元素索引值」這一推斷,也能解釋這一行爲。
考察以下代碼:
numList = [1,2] numListIter = iter(numList) for _ in numListIter: pass numList.append(3) # StopIteration print(next(numListIter))
當完整的 for 一個迭代器後,迭代器將耗盡,在 C++ 中,這將致使頭尾迭代器相等,但由上述代碼可知, Python 的迭代器一旦耗盡,便再也不可使用,即便繼續往容器中增長元素也不行。
因而可知, Python 的迭代器中可能存在某種用於指示迭代器是否被耗盡的標記,一旦迭代器被標記爲耗盡狀態,便永遠不可繼續使用了。
考察以下代碼:
numDict = {1:2} numDictIter = iter(numDict) numDict[3] = 4 # RuntimeError next(numDictIter)
當對一個 Dict 進行插入操做後,原 Dict 迭代器將當即失效,並拋出 RuntimeError。這與 C++ 中的行爲是一致的,且更爲安全。
Set 與 Dict 具備相同的迭代器失效性質,再也不重複討論。
迭代器的故事到這裏就結束了。總的看來,Python 中的迭代器雖應用普遍,但並非一種高級的,靈活的實現,且存在着一些黑魔法。 故惟有深刻的去理解,才能真正的用好迭代器。祝編程愉快~
(花下貓注:鑑於有同窗看完本文,可能想要加羣交流,我補充兩句。咱們羣雖然是免費羣,但一直想走高質量的技術交流路線,所以既限制人數,也嚴審覈。公衆號菜單欄有我聯繫方式,感興趣的同窗歡迎查看了解。)
公衆號【Python貓】, 本號連載優質的系列文章,有喵星哲學貓系列、Python進階系列、好書推薦系列、技術寫做、優質英文推薦與翻譯等等,歡迎關注哦。