花下猫语:以前说过,我对于编程语言跟其它学科的融合很是感兴趣,但我还说漏了一点,就是我对于 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进阶系列、好书推荐系列、技术写做、优质英文推荐与翻译等等,欢迎关注哦。