Python 迭代器和 C++ 迭代器,最大的不同竟然是……

2020-12-15 CSDN

作者 | 櫻雨樓

責編 | 胡巍巍

前言

迭代器(Iterator)是 Python 以及其他各種程式語言中的一個非常常見且重要,但又充滿著神秘感的概念。無論是 Python 的基礎內置函數,還是各類高級話題,都處處可見迭代器的身影。

那麼,迭代器究竟是怎樣的一個概念?其又為什麼會廣泛存在於各種程式語言中?本文將基於 C++ 與 Python,深入討論這一系列問題。

什麼是迭代器?我們為什麼要使用迭代器?

什麼是迭代器?當我初學 Python 的時候,我將迭代器理解為一種能夠放在「for xxx in …」的「…」位置的東西;後來隨著學習的深入,我了解到迭代器就是一種實現了迭代器協議的對象;學習 C++ 時,我了解到迭代器是一種行為和指針類似的對象…

事實上,迭代器是一個伴隨著迭代器模式(Iterator Pattern)而生的抽象概念,其目的是分離並統一不同的數據結構訪問其中數據的方式,從而使得各種需要訪問數據結構的函數,對於不同的數據結構可以保持相同的接口。

在很多討論 Python 迭代器的書籍與文章中,我看到這樣兩種觀點:1. 迭代器是為了節約數據結構所產生的內存;2. 遍歷迭代器效率更高。

這兩點論斷都是很不準確的:首先,除了某些不定義在數據結構上的迭代器(如文件句柄,itertools 模塊的 count、cycle 等無限迭代器等),其他迭代器都定義在某種數據結構上,所以不存在節約內存的優勢;其次,由於迭代器是一種高度泛化的實現,其需要在每一次迭代器移動時都做一些額外工作(如 Python 需要不斷檢測迭代器是否耗盡,並進行異常監測;C++ 的 deque 容器需要對其在堆上用於存儲的多段不連續內存進行銜接等),故遍歷迭代器的效率一定低於或幾乎接近於直接遍歷容器,而不太可能高於直接遍歷原容器。

綜上所述,迭代器存在的意義,不是為了空間換時間,也不是為了時間換空間,而是一種適配器(Adapter)。迭代器的存在,使得我們可以使用同樣的 for 語句去遍歷各種容器,或是像 C++ 的 algorithm 模塊所示的那樣,使用同樣的接口去處理各種容器。

這些容器可以是一個連續內存的數組或列表,或是一個多段連續內存的 deque,甚至是一個完全不連續內存的鍊表或是哈希表等等,我們完全不需要關注迭代器對於不同的容器究竟是怎麼取得數據的。

C++中的迭代器

3.1 泛化指針

在 C++ 中,迭代器通過泛化指針(Generalized Pointer)的形式呈現。泛化指針與仿函數(Functor)的定義類似,其包含以下兩種情況:

是一個真正的指針不是指針,但重載了某些指針運算符(如「*,++,--,!=」 等),使得其行為和指針相似根據泛化指針為了將其「偽裝」成一個真正的指針從而重載的運算符的數量,迭代器被分為五種,如下文所示。

3.2 C++的迭代器分類

C++ 的迭代器按其所支持的行為被分為五類:

輸入迭代器(Input Iterator):僅可作為右值(rvalue),不可作為左值(lvalue)。可以進行比較(「== 與 !=」)輸出迭代器(Output Iterator):僅可作為左值,不可作為右值前向迭代器(Forward Iterator):支持一切輸入迭代器的操作,以及單步前進操作(++)雙向迭代器(Bidirectional Iterator):支持一切前向迭代器的操作,以及單步後退操作(--)隨機訪問迭代器(Random Access Iterator):支持一切雙向迭代器操作,以及非單步雙向移動操作對於前向迭代器,雙向迭代器,以及隨機訪問迭代器,如果其不存在底層 const(Low-Level Const)限定,則同時也支持一切輸出迭代器操作。

3.3 迭代器適配器

C++ 中還存在一系列迭代器適配器,用於使得一些非迭代器對象的行為類似於迭代器,或修改迭代器的一些默認行為,大致包含如下幾個類別:

插入迭代器(Insert Iterator):使得對迭代器左值的寫入操作變為向容器中插入數據的操作,按插入位置的不同,可分為 front_insert_iterator,back_insert_iterator 和 insert_iterator反向迭代器(Reverse Iterator):對調迭代器的移動方向。使得「+」操作變為向左移動,同時「-」操作變為向右移動(類似於 Python 的 reversed 函數)移動迭代器(Move Iterator):使得對迭代器的取值變為右值引用(Rvalue Reference)流迭代器(Stream Iterator):使流對象的行為適配迭代器(類似於 Python 的文件句柄)

Python中的迭代器

4.1 迭代器協議

在 Python 中,迭代器基於鴨子類型(Duck Type)下的迭代器協議(Iterator Protocol)實現。迭代器協議規定:如果一個類想要成為可迭代對象(Iterable Object),則其必須實現__iter__方法,且其返回值需要是一個實現了__next__方法的對象。

即:實現了__iter__方法的類將成為可迭代對象,而實現了__next__方法的類將成為迭代器。

顯然,__iter__方法是 iter 函數所對應的魔法方法,__next__方法是 next 函數所對應的魔法方法。

對於一個可迭代對象,針對「誰實現了__next__方法?」這一問題進行討論,可將可迭代對象的實現分為兩種情況:

self 未實現__next__:如果__iter__方法的返回值就是一個 Iterator,則此時 self 即為一個可迭代對象。此時,self 將迭代操作「委託」到了另一個數據結構上。示例代碼如下:classSampleIterator:def__iter__(self):return iter(...)

self 實現了__next__:如果__iter__方法返回 self,則說明 self 本身將作為迭代器,此時 self 本身需要繼續實現__next__方法,以實現完整的迭代器協議。示例代碼如下:classSampleIterator:def__iter__(self):return selfdef__next__(self):# Not The Endif ...:return ...# Reach The Endelse:raise StopIteration

此例可以看出,當迭代器終止時,通過拋出 StopIteration 異常告知 Python 迭代器已耗盡。

4.2 生成器

生成器(Generator)是 Python 特有的一組特殊語法,其主要目的為提供一個基於函數而不是類的迭代器定義方式。同時,Python 也具有生成器推導式,其基於推導式語法快速建立迭代器。生成器一般適用於需要創建簡單邏輯的迭代器的場合。

只要一個函數的定義中出現了 yield 關鍵詞,則此函數將不再是一個函數,而成為一個「生成器構造函數」,調用此構造函數即可產生一個生成器對象。

由此可見,如果僅討論該語法本身,而不關心實現的話:生成器只是「借用」了函數定義的語法,實際上與函數並無關係(並不代表生成器的底層實現也與函數無關)。示例代碼如下:

def SampleGenerator():yield ...yield ...yield ...

生成器推導式則更為簡單,只需要將列表推導式的中括號換為小括號即可:

(... for ... in ...)

綜上所述,生成器是 Python 獨有的一類迭代器的特殊構造方式。生成器一旦被構造,其會自動實現完整的迭代器協議。

4.3 無限迭代器

itertools 模塊中實現了三個特殊的無限迭代器(Infinite Iterator):count,cycle 以及 repeat,其有別於普通的表示範圍的迭代器。如果對無限迭代器進行迭代將導致無限循環,故無限迭代器通常只可使用 next 函數進行取值。

4.4 與C++迭代器的比較

經過上文的討論可以發現,Python 只有一種迭代器,此種迭代器只能進行單向,單步前進操作,且不可作為左值。故 Python 的迭代器在 C++ 中應屬於單向只讀迭代器,這是一種很低級的迭代器。

此外,由於迭代器只支持單向移動,故一旦向前移動便不可回頭,如果遍歷一個已耗盡迭代器,則 for 循環將直接退出,且無任何錯誤產生,此種行為往往會產生一些難以察覺的 bug,實際使用時請務必注意。

綜上所述,Python 對於迭代器的實現其實是高度匱乏的,應謹慎使用。

迭代器有效性

5.1 什麼是迭代器有效性?

由於迭代器本身並不是獨立的數據結構,而是指向其他數據結構中的值的泛化指針,故和普通指針一樣,一旦指針指向的內存發生變動,則迭代器也將隨之失效。

如果迭代器指向的數據結構是只讀的,則顯然,直到析構函數被調用,迭代器都不會失效。但如果迭代器所指向的數據結構在其存在時發生了插入或刪除操作,則迭代器將可能失效。故討論某個操作是否會導致指向容器的迭代器失效,是一個很重要的話題。

5.2 C++的迭代器有效性

由於 Python 中沒有 C++ 的 list、deque 等數據結構實現,故本文只簡單地討論 vector 與 unordered_map 這兩種數據結構的迭代器有效性。

對於 vector,由於其存在內存擴容與轉移操作,故任何會潛在導致內存擴容的方法都將損壞迭代器,包括 push_back、emplace_back、insert、emplace 等。

unordered_map 與 vector 的情形類似,對 unordered_map 進行任何插入操作也將損壞迭代器。

5.3 Python的迭代器有效性

註:本節所討論全部內容均基於實際行為進行猜想和推論,並沒有經過對 Python 原始碼的考察和驗證,僅供讀者參考。

5.3.1 尾插入操作不會損壞指向當前元素的List迭代器

考察如下代碼:

numList = [1, 2, 3]numListIter = iter(numList)next(numListIter)for i in range(1000000):numList.append(i)# print 2print(next(numListIter))

如果在 C++ 中對一個 vector 執行這麼多次的 push_back,則指向第二個元素的迭代器一定早已失效。但在 Python 中可以看到,指向 List 的迭代器並未失效,其仍然返回了 2。

故可猜想:Python 對於 List 所產生的迭代器並不跟蹤指向 List 元素的指針,而僅僅跟蹤的是容器的索引值。

5.3.2 尾插入操作會損壞List尾迭代器

numList = [1,2]numListIter = iter(numList)# 1next(numList)numList.append(3)# 2 next(numListIter)# 3print(next(numListIter))

首先,Python 不存在尾迭代器這一概念。但由上述代碼可知,當迭代器所指向的 List 變長後,迭代器的終止點也隨之變化,即:原先的尾迭代器將不再適用。

按照「迭代器僅跟蹤元素索引值」這一推斷,也能解釋這一行為。

5.3.3 迭代器一旦耗盡,則將永久損壞

考察如下代碼:

numList = [1,2]numListIter = iter(numList)for _ in numListIter:passnumList.append(3)# StopIterationprint(next(numListIter))

當 for 一個迭代器後,迭代器將耗盡,在 C++ 中,這將導致頭尾迭代器相等,但由上述代碼可知, Python 的迭代器一旦耗盡,便不再可以使用,即使繼續往容器中增加元素也不行。

由此可見, Python 的迭代器中可能存在某種用於指示迭代器是否被耗盡的標記,一旦迭代器被標記為耗盡狀態,便永遠不可繼續使用了。

5.3.4 任何插入操作都將損壞Dict迭代器

考察如下代碼:

numDict = {1:2}numDictIter = iter(numDict)numDict[3] = 4# RuntimeErrornext(numDictIter)

當對一個 Dict 進行插入操作後,原 Dict 迭代器將立即失效,並拋出 RuntimeError。這與 C++ 中的行為是一致的,且更為安全。

Set 與 Dict 具有相同的迭代器失效性質,不再重複討論。

後記

迭代器的故事到這裡就結束了。總的看來,Python 中的迭代器雖應用廣泛,但並不是一種高級的,靈活的實現,且存在著一些黑魔法。故唯有深入的去理解,才能真正的用好迭代器。祝編程愉快~

作者:豌豆花下貓,某985高校畢業生, 兼具極客思維與人文情懷。公眾號Python貓,專注Python技術、數據科學和深度學習,力圖創造一個有趣又有用的學習分享平臺。

聲明:本文為作者投稿,版權歸作者所有。

【End】

下面給大家推薦 CSDN 的好朋友——程序人生

為什麼推薦程序人生?

程序人生聚集百萬程式設計師,在這裡你可以笑談開發軼事,吐槽百味的程序人生。

無論是從行業熱點到經驗解析,從職場困惑到風口趨勢,還有程式設計師不為人知的秘密,我們將為你一一揭曉。

相關焦點

  • 一文搞懂Python迭代器和生成器
    很多童鞋搞不懂python迭代器和生成器到底是什麼?它們之間又有什麼樣的關係?
  • python中的yield和return—迭代器和生成器
    文章目錄很多人學習python,不知道從何學起。很多人學習python,掌握了基本語法過後,不知道在哪裡尋找案例上手。
  • 面試題-python 什麼是迭代器?
    前言python 裡面有 3 大神器:迭代器,生成器,裝飾器。
  • 什麼是Python的迭代器和生成器?(附代碼)
    Python中的生成器和迭代器。在處理大量數據時,計算機內存可能不足,我們可以通過生成器和迭代器來解決該問題。迭代器:一次一個!Python 是一種美麗的程式語言。我喜歡它提供的靈活性和難以置信的功能。我喜歡深入研究Python的各種細微差別,並了解它如何應對不同的情況。在使用Python的過程中,我了解到了一些功能,這些功能的使用與其簡化的複雜度不相稱。
  • 【Python基礎】可迭代對象&迭代器對象及其實現
    首選確保for循環的in後面是一個可迭代對象,這樣就能通過python內置函數iter()得到一個迭代器對象(iterator)我們分別把列表list_test和字符串str_test分別得到一個迭代器 我們嘗試傳入數字看看
  • Python 拓展之迭代器
    那麼在這裡有一個問題,iter_list 和 list1 有區別嗎?通過上面的內容和我們之前的文章對迭代的講述,下面我們對迭代器做一個概括:1.在 Python 中,迭代器是遵循迭代協議的對象。我們可以使用 iter() 從任何序列得到迭代器(exp: list,turple,set and so on)。2.當自己編寫迭代器的類的時候,其中實現 __iter__() 和 __next__() 方法,如果沒有元素的話,會引發 StopIteration 異常。
  • C++(STL):17---deque之迭代器使用
    ,通過調用這些函數,可以獲得表示不同含義的隨機訪問迭代器。rbegin()返回指向最後一個元素的反向迭代器;如果是 const 類型容器,在該函數返回的是常量反向迭代器。rend()返回指向第一個元素之前一個位置的反向迭代器。如果是 const 類型容器,在該函數返回的是常量反向迭代器。此函數通常和 rbegin() 搭配使用。cbegin()和 begin() 功能類似,只不過其返回的迭代器類型為常量正向迭代器,不能用於修改元素。
  • Python中迭代器和生成器的區別?
    如果參考答案不夠好,或者有錯誤的話,麻煩大家可以在留言區給出自己的意見和討論,大家是要一起學習的 。廢話不多說,開始今天的題目:問:說說Python中迭代器和生成器的區別?答:Python中生成器能做到迭代器能做的所有事,而且因為自動創建了__iter__()和next()方法,生成器顯得特別簡潔,而且生成器也是高效的,使用生成器表達式取代列表解析,同時節省內存。除了創建和保持程序狀態的自動生成,當發生器終結時,還會自動跑出StopIterration異常。
  • ES6的生成器和迭代器
    其中兩個特性,生成器和迭代器,極大地改變了我們在更複雜的前端代碼中編寫特定函數的方式。雖然他們之間的關係很好,但他們實際上做的事情可能有點令人困惑,所以讓我們來看看他們。迭代器迭代在編程中是一種常見的做法,通常用於循環一組值,要麼轉換每個值,要麼使用或以某種方式保存它。
  • 傳智播客鄭州校區Python學習之迭代器與生成器
    三、傳智播客Python學習:迭代器迭代器和遞歸函數的區別:遞歸函數是不斷的重複調用自己,必須有一個明確的條件,而且每進行更深一層的循環,規模一定要較之前要小,迭代器,每次循環都要依賴於上一次的結果。
  • Python小白教程:生成器 (generator) 和迭代器 (iterator)
    接下來我們做個小實驗,對比一下生成器和列表在運行簡單操作時的用的時間和佔的內存。首先引入記錄運行時間的 time 和佔內存大小的 sys。做 1 千萬次 x+1 的操作,生成列表 l 和生成器 g。我們已經知道迭代器裡面有 __iter__ 和 __next__ 方法,那我們只需在類裡自定義這兩個方法了。以 MyRange 為例。
  • 雲計算開發學習筆記:Python3迭代器與生成器
    迭代器是一個可以記住遍歷的位置的對象。迭代器對象從集合的第一個元素開始訪問,直到所有的元素被訪問完結束。迭代器只能往前不會後退。迭代器有兩個基本的方法:iter() 和 next()。字符串,列表或元組對象都可用於創建迭代器:迭代器對象可以使用常規for語句進行遍歷:執行以上程序,輸出結果如下:也可以使用 next() 函數:執行以上程序,輸出結果如下:
  • Python從小白進階到大神的三大利器之迭代器
    點擊運行:可以看出,Python將list裡面的數據給列印出來,像這樣通過for循環遍歷列表元素的過程就叫做迭代,在Python裡面由於for語句的強大之處,迭代器更多的是用在迭代和遍歷上實際上,在Python中,迭代器已經被弱化了,很少有機會自己定義一個迭代器,在進行迭代,因為我們的for在配合可遍歷的對象可以完成我們大部分工作,但是由於迭代器作為Python的從小白到大神的三大利器之一,今天我們就來看看Python的迭代器是怎麼實現的?
  • python他律筆記系列二
    語法if語句  if-elif-else 與c/c++ 不同的是每個條件後面需要使用冒號:另外python中無switch-case語句while語句 與c/c++一致所需要注意的就是需要加上冒號:另外python中無do...while循環對於 while 1這類死循環可以通過crtl+c強制退出但是有while...else 語句for語句
  • 迭代器與生成器 (一)
    迭代器(iterator)有了前面的概念,就可以探討迭代器了。按照GOF(四人幫GANG OF FOUR,即《設計模式》一書的作者Erich Gamma, Richard Helm,Ralph Johnsonh和John Vlissides)的定義,迭代器的作用是提供一種方法,去訪問一個容器對象中的各個元素,而又不需要暴露該對象的內部細節。
  • 一文詳解枚舉器和迭代器!
    同理字典也可以通過集合初始化器進行對象初始化和元素填充。迭代器簡化了對象間的通信,使得不關心序列的類型,而獲得序列中的每個元素。C#中迭代器被IEnumerator和IEnumerable和其對應的泛型接口封裝。一個類如果實現了IEnumerable接口,那麼就能夠被迭代;調用GetEnumerator方法將返回IEnumerator接口的實現,它就是迭代器本身。
  • 精讀《設計模式 - Iterator 迭代器模式》
    這種設計模式要解決的根本問題是,聚合的種類有很多,比如對象、鍊表、數組、甚至自定義結構,但遍歷這些結構時,不同結構的遍歷方式又不同,所以我們必須了解每種結構的內部定義才能遍歷。這種模式和 Array.from 有點像,但其實真正的迭代器在 JS 裡是 obj[Symbol.iterator](),也就是一個對象實現了 [Symbol.interator],就認為是可遍歷的。
  • C++中,date_time庫示範日期迭代器的基本用法,都在這裡了
    date_time庫可以用簡單的遞增或者遞減操作符連續訪問日期,這些迭代器包括day_iterator、week_iterator、month_iterator和year_iterator,它們分別以天、周、月和年為單位增減。
  • Python中可迭代對象、迭代器以及iter函數的兩個用法詳解
    迭代器 迭代器是一個可以記住遍歷的位置的對象。 迭代器對象從集合的第一個元素開始訪問,直到所有的元素被訪問完結束。迭代器只能往前不會後退。
  • 【探秘ES6】系列專欄(二):迭代器和for-of循環
    // make a set from an array of words var uniqueWords = new Set(words);如果你想遍歷你的set,很簡單:for (var word of uniqueWords) { console.log(word); }Map有一點不同