迭代器

迭代是什么

迭代指的是一个重复的过程,每次重复都必须基于上一次的结果而继续,单纯的重复并不是迭代,如Python中的for循环就是一个非常好的迭代例子。

for item in range(10):
print(item)

迭代必须向前推进,不能后退,如下所示:

# [0 , 1, 2, 3, 4, 5, 6, 7, 8, 9]
# ------------------------------>

下面这种方式就不属于迭代:

# [0 , 1, 2, 3, 4, 5, 6, 7, 8, 9]
# -------->
# <----
# --------------------->

迭代器协议

在学习迭代器的整个知识点中,迭代器协议占据了非常重要的位置。

迭代器协议中包含了2个最基本的概念,分别是可迭代对象和迭代器对象。

  • 可迭代对象(Iterable):内部实现了__iter__()方法的对象则被称为可迭代对象
  • 迭代器对象(Iterator):内部实现了__next__()方法的对象则被称之为迭代器对象

两者之间的关系:

  • 在Python中,迭代器对象一定属于可迭代对象范畴,也就说迭代器对象必须具有__iter__()方法以及__next__()方法
  • 在Python中,可迭代对象不一定属于迭代器对象范畴,也就是说可迭代对象只需要实现__iter__()方法即可

介绍2个函数:

  • iter(Object)函数,它底层会执行Object.__iter__()方法
  • next(Object)函数,它底层会执行Object.__next__()方法

内置类型

通过collections.abc下的Iterable类和Iterator类进行判定,可快速的判定出所有内置类型是否是一个可迭代对象或者迭代器对象:

>>> from collections.abc import Iterable
>>> from collections.abc import Iterator >>> isinstance(list(), Iterable)
True
>>> isinstance(list(), Iterator)
False

经过测试,所有的容器类型(list、tuple、str、dict、set、frozenset)均属于可迭代对象,但不属于迭代器对象

原子类型(bool、int、float、None)等均不属于可迭代对象,更不属于迭代器对象。

也可以通过另一种方式进行验证,通过hasattr()函数,检查类中是否定义了某一个方法:

>>> hasattr(list,"__iter__")
True
>>> hasattr(list,"__next__")
False

for循环原理

当可迭代对象被for循环进行调用后,底层执行流程如下所示:

  1. 将自动的执行iter()方法,该方法内部会查找可迭代对象的__iter__()方法,如果具有该方法,则返回一个该可迭代对象的专属迭代器对象,如果没有该方法,则抛出TypeError object is not iterable的异常。

    Ps:每次的for循环都会返回一个全新的迭代器对象

  2. 不断的调用迭代器对象的__next__()方法,并且返回迭代器对象中下一个数据项,当遍历完成整个迭代器后,引发Stopiteration异常终止迭代

    Ps:迭代器本身并不存储任何数据项,存储的只是一个指针,该指针指向可迭代对象中真正存储的数据项,它指向当前被遍历到的数据项索引位置,下一次遍历则向后推进这个位置

  3. for循环自动的捕捉Stopiteration异常,并且停止迭代

    Ps:for循环底层就是while循环实现的,只不过多加了3个步骤:

    第一步:执行可迭代对象的__iter()__方法并保存返回的专属迭代器

    第二步:不断的执行迭代器的__next()__方法

    第三步:捕获Stopiteration异常

我们手动的实现一个for循环:

li1 = list(range(10))

iteratorObject = iter(li1)  # ❶
while 1:
try:
print(next(iteratorObject)) # ❷
except StopIteration as e: # ❸
break

❶:执行可迭代对象的__iter__()方法并保存返回的专属迭代器

❷:不断的执行迭代器的__next__()方法

❸:捕获Stopiteration异常

线性可迭代对象与迭代器的实现

如果是一个线性容器的可迭代对象,那么它一定具有索引值,我们可以让它的__iter__()方法返回一个专属的迭代器对象。

然后专属迭代器对象中记录本次迭代遍历的索引值,根据这个索引值返回可迭代对象中的数据项,当索引值达到可迭代对象中数据项总个数-1的时候,抛出异常,本次迭代结束:

class linearTypeContainer:
def __init__(self, array):
if isinstance(array, list) or isinstance(array, tuple):
self.array = array
else:
raise TypeError("argument array must is linear container") def __iter__(self):
return linearContainer_iterator(self.array) # ❶ class linearContainer_iterator:
def __init__(self, array):
self.index = 0
self.array = array
self.len = len(self.array) def __next__(self):
if self.index < self.len:
retDataItem = self.array[self.index]
self.index += 1
return retDataItem
else:
raise StopIteration def __iter__(self): # ❷
return self container = linearTypeContainer([i for i in range(10)])
for i in container:
print(i)
print(list(container))

❶:Python中的一切传参均为引用传递

故linearTypeContainer中的self.array和linearContainer_iterator的self.array都是一个对象,并不会额外开辟内存空间

这也就是为什么可迭代对象创建的专属迭代器不会消耗太多的内存空间原因了。

❷:迭代器对象一定属于可迭代对象范畴,所以在这里我们为迭代器对象linearContainer_iterator类也新增了__iter__()方法

这样做的好处在于如果单独的拎出了这个迭代器对象,则它也会支持for循环的遍历:

    def __iter__(self):  # ❷
return self containerIterator = linearTypeContainer([i for i in range(10)]).__iter__() for item in containerIterator:
print(item) # 0
# 1
# 2
# 3
# 4
# 5
# 6
# 7
# 8
# 9

如果取消了linearContainer_iterator类的这个__iter__()方法,则不支持for循环的遍历:

...
# def __iter__(self): # ❷
# return self containerIterator = linearTypeContainer([i for i in range(10)]).__iter__() for item in containerIterator:
print(item) # TypeError: 'linearContainer_iterator' object is not iterable

非线性可迭代对象与迭代器实现

如果是一个非线性容器的可迭代对象,可以先判断它的类型,如果传入的容器是一个字典,则将迭代的数据项集合转换为元组,里面存储的全部是字典的key即可。

如果传入的容器是一个集合,则将迭代的数据项集合转换为元组,再参照线性可迭代对象与迭代器的实现。

具体实现:

class mappingTypeContainer:
def __init__(self, mapping):
self.mapping = mapping
self.mappingType = None if isinstance(mapping, dict):
self.mappingType = "dict" elif isinstance(mapping, set) or isinstance(mapping, frozenset):
self.mappingType = "set" else:
raise TypeError("argument mapping must is mapping container") def keys(self):
if self.mappingType == "set":
raise TypeError("instance mapping type is set, no have method keys")
else:
return self.mapping def values(self):
if self.mappingType == "set":
raise TypeError("instance mapping type is set, no have method values")
else:
return self.mapping.values() def items(self):
if self.mappingType == "set":
raise TypeError("instance mapping type is set, no have method items")
else:
return self.mapping.items() def __str__(self):
return str(self.mapping) def __iter__(self):
return mappingContainer_iterator(tuple(self.mapping)) class mappingContainer_iterator:
def __init__(self, array):
self.index = 0
self.array = array
self.len = len(self.array) def __next__(self):
if self.index < self.len:
retDataItem = self.array[self.index]
self.index += 1
return retDataItem
else:
raise StopIteration def __iter__(self):
return self container = mappingTypeContainer({str("k") + str(i): str("v") + str(i) for i in range(3)}) for item in container.items():
print(item) print(container) # ('k0', 'v0')
# ('k1', 'v1')
# ('k2', 'v2')
# {'k0': 'v0', 'k1': 'v1', 'k2': 'v2'} container = mappingTypeContainer({i for i in range(3)}) for item in container:
print(item) print(container) # 0
# 1
# 2
# {0, 1, 2}

迭代器对象的特性

每一次for循环创建出的可迭代对象的专属迭代器都是一次性的,用完后就没用了:

# ❶
containerIterator = linearTypeContainer([i for i in range(3)]).__iter__() for item in containerIterator:
print(item) # 0
# 1
# 2 for item in containerIterator:
print(item) # ❷
print("?")

❶:直接拿出一个迭代器对象

❷:在第2次循环中,迭代器对象中存储的索引值已经最大了,每次调用iter()都会抛出异常返回出来再被for处理,所以print()函数根本不会运行

迭代器对象并不存储可迭代对象中的真正迭代数据,而是仅存储长度和索引,所以内存的占用并不多:

class linearContainer_iterator:
def __init__(self, array):
self.index = 0 # ❶
self.array = array # ❷
self.len = len(self.array) # ❸ ...

❶:占用额外的内存空间

❷:引用对象,并不开辟内存

❸:占用额外的内存空间

惰性求值与及早求值

迭代器对象中对于返回的数据项,是进行实时演算的,这种实时演算的特性求值方式被称为惰性求值,即你需要的时候我算出来后再给你:

    def __next__(self):
if self.index < self.len:
retDataItem = self.array[self.index]
self.index += 1
return retDataItem
else:
raise StopIteration

除开惰性求值,还有一种及早求值的方案,即使你要1个,我也把所有的都给你。

如Python2中的range()、map()、filter()、dict.items()、dict.keys()、dict.values(),它们均返回的是一个纯粹的列表,这样的设计是不合理的。

因为返回的列表会占用很大的内存空间,而Python3中则统一优化为惰性求值方案,即返回一个可迭代对象。

要命的问题

①:Python中的所有自带容器类型为何不自己设置成迭代器?

而是在for循环时实例出一个专属的迭代器?

直接在这些自带类型的底层实现__next__()方法不好吗?

这样岂不是更加减少了内存的消耗,少定义了类和实例化了类吗?

答:这真是一个要命的问题,这个问题我也想过很久,最后是在stackoverflow提问并且获得了良好的解答才记录下来的。

因为确实是可以实现的,如下所示,只需要在加上❶处代码即可:

class linearTypeContainer:
def __init__(self, array):
if isinstance(array, list) or isinstance(array, tuple):
self.array = array
else:
raise TypeError("argument array must is linear container") self.index = 0
self.len = len(self.array) def __iter__(self):
return self def __next__(self):
if self.index < self.len:
retDataItem = self.array[self.index]
self.index += 1
return retDataItem
else:
self.index = 0 # ❶
raise StopIteration container = linearTypeContainer(list(range(5))) for item in container:
print(item) for item in container:
print(item) for item in container:
print(item)

但是这样做在某种特殊情况下会出现问题:

container = linearTypeContainer(list(range(5)))

for item in container:
print(item)
if container.index == 3:
break print("*"*20) for item in container:
print(item) # 0
# 1
# 2
# ********************
# 3
# 4

你会发现如果第一次for循环到了1半的时候退出,第二次for循环会接着根据第一次for循环进行继续。

能够解决一下吗?只需要加上一个标志位即可:

class linearTypeContainer:
def __init__(self, array):
if isinstance(array, list) or isinstance(array, tuple):
self.array = array
else:
raise TypeError("argument array must is linear container") self.index = 0
self.len = len(self.array)
self.iter = False # ❶ def __iter__(self):
if self.iter: # ❷
self.index = 0
self.iter = True
return self def __next__(self):
if self.index < self.len:
retDataItem = self.array[self.index]
self.index += 1
return retDataItem
else:
self.index = 0
raise StopIteration container = linearTypeContainer(list(range(5))) for item in container:
print(item)
if container.index == 3:
break print("*" * 20) for item in container:
print(item) # 0
# 1
# 2
# ********************
# 0
# 1
# 2
# 3
# 4

❶:判断是不是一次新的调用

❷:如果是新的调用,则将index重新置为0即可

那么为何Python不这样设计呢?我们应该更多的考虑多线程的情况下,多个for循环使用同一个迭代器它是否是线程安全的,上面的示例中这个共享迭代器并不是线程安全的,此外它也不支持嵌套循环,如下所示,这样会造成无限循环:

container = linearTypeContainer(list(range(5)))

for item in container:
print(item)
for j in container:
print(j)

综上各个方面的考虑,Python将内置的数据类型,都设置了在for循环时返回专属迭代器的做法,这是非常好的设计,但是对于有些内置的对象,则是将它本身做成了迭代器,如文件对象。

②:Python2中返回的及早求值对象,就没有一点好处吗?真的是浪费内存百无一用?

答:也不是,你可以发现Python3中所有返回的及早求值对象,都不支持索引操作,但是Python2中返回的由于是列表,它能够支持索引操作,在某些极度极端的情况下这确实是个优势,但是Python3的惰性求值对象需要这种优势吗?你手动将它转换为list不香吗?这样提供给了你更多操作性的同时优化了内存占用,何乐而不为呢?

③:你能实现一个返回惰性求值的对象吗?

答:能啊!你看,我实现一个Range吧,其实就是传参位置和自带的不一样,但是它是线程安全的且支持嵌套循环的:

class Range:
def __init__(self, stop, start=0, step=1):
self.start = start
self.stop = stop
self.step = step
self.current = None def __iter__(self):
return Range_iterator(self.stop, self.start, self.step) class Range_iterator:
def __init__(self, stop, start, step):
self.start = start
self.stop = stop
self.step = step
self.current = self.start def __next__(self):
if self.current < self.stop:
retDataItem = self.current
self.current += self.step
return retDataItem
raise StopIteration for i in Range(10):
print(i)
for j in Range(10):
print(j)

深度理解Python迭代器的更多相关文章

  1. 怎么理解Python迭代器与生成器?

    怎么理解Python迭代器与生成器?在Python中,使用for ... in ... 可以对list.tuple.set和dict数据类型进行迭代,可以把所有数据都过滤出来.如下:         ...

  2. 深度理解python中的元类

    本文转自:(英文版)https://stackoverflow.com/questions/100003/what-is-a-metaclass-in-python   (翻译版)   http:// ...

  3. 一文搞懂Python迭代器和生成器

    很多童鞋搞不懂python迭代器和生成器到底是什么?它们之间又有什么样的关系? 这篇文章就是要用最简单的方式让你理解Python迭代器和生成器! 1.迭代器和迭代过程 维基百科解释道: 在Python ...

  4. 完全理解 Python 迭代对象、迭代器、生成器(转)

    完全理解 Python 迭代对象.迭代器.生成器 本文源自RQ作者的一篇博文,原文是Iterables vs. Iterators vs. Generators » nvie.com,俺写的这篇文章是 ...

  5. 完全理解 Python 迭代对象、迭代器、生成器

    完全理解 Python 迭代对象.迭代器.生成器 2017/05/29 · 基础知识 · 9 评论 · 可迭代对象, 生成器, 迭代器 分享到: 原文出处: liuzhijun    本文源自RQ作者 ...

  6. 大数据学习day15----第三阶段----scala03--------1.函数(“_”的使用, 函数和方法的区别)2. 数组和集合常用的方法(迭代器,并行集合) 3. 深度理解函数 4 练习(用java实现类似Scala函数式编程的功能(不能使用Lambda表达式))

    1. 函数 函数就是一个非常灵活的运算逻辑,可以灵活的将函数传入方法中,前提是方法中接收的是类型一致的函数类型 函数式编程的好处:想要做什么就调用相应的方法(fliter.map.groupBy.so ...

  7. Python迭代器,生成器--精华中的精华

    1. 迭代器 迭代器是访问集合元素的一种方式.迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束.迭代器只能往前不会后退,不过这也没什么,因为人们很少在迭代途中往后退.另外,迭代器的一大 ...

  8. [深度分析] Python Web 开发框架 Bottle

    [深度分析] Python Web 开发框架 Bottle(这个真的他妈的经典!!!) 作者:lhf2009913 Bottle 是一个非常精致的WSGI框架,它提供了 Python Web开发中需要 ...

  9. python -迭代器与生成器 以及 iterable(可迭代对象)、yield语句

    我刚开始学习编程没多久,对于很多知识还完全不知道,而有些知道的也是一知半解,我想把学习到的知识记录下来,一是弥补记忆力差的毛病,二也是为了待以后知识能进一步理解透彻时再回来做一个补充. 参考链接: 完 ...

随机推荐

  1. 简要说一下.Net的编译过程.

    看面试题的时候遇到这样一道题目,简要说明.NET的编译过程,在网上看了很多资料,简单总结如下: 1.一般的编译过程 通常高级语言的程序编译过程是:首先写好的程序是源代码,然后编译器编译为本地机器语言, ...

  2. 2019看雪CTF 晋级赛Q2第四题wp

    上次参加2019看雪CTF 晋级赛Q2卡在了这道题上,虽然逆出算法,但是方程不会解,哈哈哈哈,果然数学知识很重要呀,现在记录一下. 首先根据关键信息,根据错误提示字符串定位到这里: 1 int __t ...

  3. 11、MyBatis教程之动态SQL

    12.动态SQL 1.介绍 什么是动态SQL:动态SQL指的是根据不同的查询条件 , 生成不同的Sql语句. 官网描述: MyBatis 的强大特性之一便是它的动态 SQL.如果你有使用 JDBC 或 ...

  4. 力扣 - 560. 和为K的子数组

    目录 题目 思路1(前缀和) 代码 复杂度分析 思路2(前缀和+哈希表优化) 代码 复杂度分析 题目 560. 和为K的子数组 思路1(前缀和) 构建前缀和数组,可以快速计算任意区间的和 注意:计算区 ...

  5. 清明节特辑 |记忆存储、声音还原、性格模仿……AI可以让人类永生吗?

    摘要:如果能用AI "复活"逝去的亲人 你愿意吗? 清明节,很少有人会去特地想这样一个问题:我们为什么要给过世的人修墓,然后每年固定的时间去扫墓?当农耕文化的色彩褪去,清明节的祭祀 ...

  6. 学习笔记-git 上传

    0.git add * (如果你需要修改源码需要在 1 之前使用,然后再回到 1) 1.git commit -m '提交文字描述' 2.git push -u origin master (上传到主 ...

  7. SecureCRT 连接Win10内置ubuntu问题及解决办法

    1: 输入hostname, username 后连接提示: ubuntu The remote system refused the connection. 因为没有安装或启动 ssh.  使用命令 ...

  8. SpringMVC请求参数解析

    请求参数解析 客户端请求在handlerMapping中找到对应handler后,将会继续执行DispatchServlet的doPatch()方法. 首先是找到handler对应的适配器. Hand ...

  9. Maven精简笔记

    Maven基本命令 1.mvn complile:编译命令,将src/main目录下的代码以及资源文件进行编译,在项目目录中生成target目录用来存放编译好的 class 文件等等 2.mvn cl ...

  10. 1076 Forwards on Weibo

    Weibo is known as the Chinese version of Twitter. One user on Weibo may have many followers, and may ...