當談論迭代器時,我談些什么?

花下貓語:之前說過,我對于編程語言跟其它學科的融合非常感興趣,但我還說漏了一點,就是我對于 Python 跟其它編程語言的對比學習,也很感興趣。所以,我一直希望能聚集一些有其它語言基礎的同學,一起討論共通的語言特性間的話題。不同語言的碰撞,常常能帶給人更高維的視角,也能觸及到語言的根基,這個過程是極有益的。

這篇文章是群內 櫻雨樓 小姐姐的投稿,她是我們學習群里的真·大佬,說到對 Python 的研究以及高階知識的水平,無人能出其右(群里很多同學都被她實力圈粉啦)。除了 Python,她對 C++、Perl、Go 與 Fortran 等語言都有涉獵,本文主要是對比了 Python 與 C++,來深入談談迭代器。話不多說,請看正文。

68b02e3bly1g4etmmaofqj22bk1jkqkz.jpg

櫻雨樓 | 原創作者

豌豆花下貓 | 編輯潤色

本文原創并首發于公眾號【Python貓】,未經授權,請勿轉載。

原文地址:https://mp.weixin.qq.com/s/Be4tHnR0BY-C__xoPPBjhQ

0 前言

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

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

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

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

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

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

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

68b02e3bly1g4eufzzhiyg20b405w14x.gif

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

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

2 C++中的迭代器

2.1 泛化指針

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

  1. 是一個真正的指針
  2. 不是指針,但重載了某些指針運算符(如“*,++,--,!=” 等),使得其行為和指針相似

根據泛化指針為了將其“偽裝”成一個真正的指針從而重載的運算符的數量,迭代器被分為五種,如下文所示。

2.2 C++的迭代器分類

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

  1. 輸入迭代器(Input Iterator):僅可作為右值(rvalue),不可作為左值(lvalue)。可以進行比較(“== 與 !=”)
  2. 輸出迭代器(Output Iterator):僅可作為左值,不可作為右值
  3. 前向迭代器(Forward Iterator):支持一切輸入迭代器的操作,以及單步前進操作(++)
  4. 雙向迭代器(Bidirectional Iterator):支持一切前向迭代器的操作,以及單步后退操作(--)
  5. 隨機訪問迭代器(Random Access Iterator):支持一切雙向迭代器操作,以及非單步雙向移動操作

對于前向迭代器,雙向迭代器,以及隨機訪問迭代器,如果其不存在底層 const(Low-Level Const)限定,則同時也支持一切輸出迭代器操作。

2.3 迭代器適配器

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

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

3 Python中的迭代器

3.1 迭代器協議

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

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

68b02e3bly1g4eumghtomj20sg0fe3zb.jpg

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

  1. self 未實現__next__:如果__iter__方法的返回值就是一個 Iterator,則此時 self 即為一個可迭代對象。此時,self 將迭代操作“委托”到了另一個數據結構上。示例代碼如下:
class SampleIterator:def __iter__(self):return iter(...)
  1. self 實現了__next__:如果__iter__方法返回 self,則說明 self 本身將作為迭代器,此時 self 本身需要繼續實現__next__方法,以實現完整的迭代器協議。示例代碼如下:
class SampleIterator:def __iter__(self):return selfdef __next__(self):# Not The Endif ...:return ...# Reach The Endelse:raise StopIteration

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

3.2 生成器

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

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

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

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

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

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

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

3.3 無限迭代器

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

關于無限迭代器的詳細內容,可參閱 Python 文檔。(注:舊文 Python進階:設計模式之迭代器模式 也介紹過)

3.4 與C++迭代器的比較

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

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

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

4 迭代器有效性

4.1 什么是迭代器有效性?

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

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

68b02e3bly1g4eulthd9cj21hc0swgp2.jpg

4.2 C++的迭代器有效性

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

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

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

4.3 Python的迭代器有效性

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

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

考察如下代碼:

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 元素的指針,而僅僅跟蹤的是容器的索引值。

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

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

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

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

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

考察如下代碼:

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

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

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

68b02e3bly1g4euo8ldevj20sg0f4wfh.jpg

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

考察如下代碼:

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

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

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

5 后記

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

(花下貓注:鑒于有同學看完本文,可能想要加群交流,我補充兩句。我們群雖然是免費群,但一直想走高質量的技術交流路線,因此既限制人數,也嚴審核。公眾號菜單欄有我聯系方式,感興趣的同學歡迎查看了解。)

68b02e3bly1g2aiq1kpa8j21hc0nmgs4.jpg

公眾號【Python貓】, 本號連載優質的系列文章,有喵星哲學貓系列、Python進階系列、好書推薦系列、技術寫作、優質英文推薦與翻譯等等,歡迎關注哦。

轉載于:https://www.cnblogs.com/pythonista/p/11094501.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/247691.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/247691.shtml
英文地址,請注明出處:http://en.pswp.cn/news/247691.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

在Vue-cli項目中使用echarts

該示例使用 vue-cli 腳手架搭建 安裝echarts依賴 npm install echarts -S11 或者使用國內的淘寶鏡像: 安裝 npm install -g cnpm --registryhttps://registry.npm.taobao.org11 使用 cnpm install echarts -S11 創建圖表 全局引入 main.js // 引入echarts im…

Java的模板文件配置

Java的Mappers文件配置 <?xml version"1.0" encoding"UTF-8" ?><!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace"com.qfedu.…

Python爬蟲學習二

1、selenium自動測試工具 2、主要使用selenium的目的是跳過登錄驗證3、下載驅動器下載請求庫from selenium import webdriver import time#1、 直接在script文件夾中找驅動 driverwebdriver.Chrome() time.sleep(5) driver.close()#2、找到驅動路徑 #webdriver.Chrome(rD:\Pyth…

通過GitHub Pages創建個人主頁

登陸github,創建新倉庫&#xff0c;寫入名字, 這里要以github.io做后綴, 不然創建出來的不是GitHub Pages 打開終端, cd到自己想要的文件夾后clone到本地 git clone https://github.com/username/username.github.io 進入這個項目文件夾 cd username.github.io 把寫好HTML項目拷…

Spring IOC 配置文件模板

基于XML的普通設置 <?xml version1.0 encodingUTF-8 ?> <beans xmlns"http://www.springframework.org/schema/beans"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xmlns:context"http://www.springframework.org/schema/contex…

validate+jquery+ajax表單驗證

1.案例 1.1 Html form表單內容 <form class"cForm" id"cForm" method"post" action""> <p> <label for"user">用戶名</label> <input id"user" name"user" required minlen…

Html5做webapp中界面適配的問題總結

做一款軟件首先是要做出相應的界面&#xff0c;然而對于手機軟件開發者來說&#xff0c;大小各異的手機屏幕卻給我們帶來了不少的麻煩。HTML5技術在大家的心中要比傳統的Android/iOS/wp簡單的多&#xff0c;因為它的適配性和平臺跨越方面比較出色。在外看來卻不是那樣的&#x…

設置Maven下載鏡像源(直接替換其中的 settings.xml 內容即可)

<?xml version"1.0" encoding"UTF-8"?> <settings xmlns"http://maven.apache.org/SETTINGS/1.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache.org/SETTINGS/1.0.…

P1576 最小花費

----------------------------------- 這道題就是圖論最短路&#xff0c;但是我們要改一下一些細節 比如說&#xff0c;因為這是算匯率&#xff0c;我們的初始化就要是0 我們還要改一改松弛操作 ----------------------------------- 還有&#xff0c;題目上給的是匯率&#xf…

css hack技術整理

做前端多年&#xff0c;雖然不是經常需要hack&#xff0c;但是我們經常會遇到各瀏覽器表現不一致的情況。基于此&#xff0c;某些情況我們會極不情愿的使用這個不太友好的方式來達到大家要求的頁面表現。我個人是不太推薦使用hack的&#xff0c;要知道一名好的前端&#xff0c;…

Hanoi雙塔問題

Hanoi雙塔問題 題目描述 給定A,B,C三根足夠長的細柱&#xff0c;在A柱上放有2n個中間有空的圓盤&#xff0c;共有n個不同的尺寸&#xff0c;每個尺寸都有兩個相同的圓盤&#xff0c;注意這兩個圓盤是不加區分的(下圖為n3的情形&#xff09;。現要將 這些國盤移到C柱上&#xff…

vue中config/index.js:配置的詳細理解

當我們需要和后臺分離部署的時候&#xff0c;必須配置config/index.js: 用vue-cli 自動構建的目錄里面 &#xff08;環境變量及其基本變量的配置&#xff09; 123456789101112131415var path require(path)module.exports {build: {index: path.resolve(__dirname, dist/ind…

uni-app吸頂固定樣式

<template><view class"full"><view class"sticky-box"><!-- 搜索 --><uni-search-bar class"unisearchbar" radius"5" placeholder"請輸入搜索關鍵詞" clearButton"auto" bgColor&qu…

Django(模板語言-自定義filter和simple_tag)

filter過濾器的主要形式&#xff1a;變量|函數,意思是將變量交給函數處理&#xff0c;而自定義filter就是自己定義函數&#xff0c;因為用到已有的很少。 1.在當前app中創建templatetags模塊(包&#xff1a;帶__init__.py)&#xff08;必須的&#xff09; 2.在templatetags中創…

uni-app商品分類

<template><view class"classify"><!-- 分類部分 --><view class"cate-left"><view :class"[cate-item,activeIndexindex?active:]" v-for"(item,index) in cateList" :key"index"click"c…

10分鐘看懂瀏覽器的渲染過程及優化

一、瀏覽器概述 目前的主流瀏覽器有5個&#xff1a;Internet Explorer、Firefox、Safari、Chrome和Opera瀏覽器。根據 StatCounter 瀏覽器統計數據&#xff0c;目前&#xff08;截止2019 年 5 月&#xff09;Firefox、Safari 和 Chrome 瀏覽器的總市場占有率將近 83.66%。由此可…

淺談 Vue 項目優化

前幾天看到大家說 vue 項目越大越難優化&#xff0c;帶來很多痛苦&#xff0c;這是避免不了的&#xff0c;問題終究要解決&#xff0c;框架的性能是沒有問題的&#xff0c;各大測試網站都有相關數據。下面進入正題 基礎優化 所謂的基礎優化是任何 web 項目都要做的&#xff0c;…

uni-app微信小程序一鍵登錄獲取權限功能

<button class"bottom size_30" type"primary" lang"zh_CN" click"getUserInfo">一鍵登錄</button>//第一授權獲取用戶信息》按鈕觸發getUserInfo() {// 展示加載框uni.showLoading({title: 加載中,});uni.getUserProfile…

第九章 結構體與共用體

姓名&#xff1a;呂家浩 實驗地點&#xff1a;教學樓514教室 實驗時間&#xff1a;4月30日 一、本章要點 1.通過實驗理解結構體和共用體的數據結構2.結構體、共用體中數組的使用及變量的賦值3.結構體和共用體定義時的嵌套使用&#xff08;嵌套使用的結構體必須先定義&…

H5-localStorage數據存儲總結

一、什么是localStorage、sessionStorage 在HTML5中&#xff0c;新加入了一個localStorage特性&#xff0c;這個特性主要是用來作為本地存儲來使用的&#xff0c;解決了cookie存儲空間不足的問題(cookie中每條cookie的存儲空間為4k)&#xff0c;localStorage中一般瀏覽器支持的…