深度理解Python迭代器
迭代器
迭代是什么
迭代指的是一个重复的过程,每次重复都必须基于上一次的结果而继续,单纯的重复并不是迭代,如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循环进行调用后,底层执行流程如下所示:
将自动的执行iter()方法,该方法内部会查找可迭代对象的__iter__()方法,如果具有该方法,则返回一个该可迭代对象的专属迭代器对象,如果没有该方法,则抛出TypeError object is not iterable的异常。
Ps:每次的for循环都会返回一个全新的迭代器对象
不断的调用迭代器对象的__next__()方法,并且返回迭代器对象中下一个数据项,当遍历完成整个迭代器后,引发Stopiteration异常终止迭代
Ps:迭代器本身并不存储任何数据项,存储的只是一个指针,该指针指向可迭代对象中真正存储的数据项,它指向当前被遍历到的数据项索引位置,下一次遍历则向后推进这个位置
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迭代器的更多相关文章
- 怎么理解Python迭代器与生成器?
怎么理解Python迭代器与生成器?在Python中,使用for ... in ... 可以对list.tuple.set和dict数据类型进行迭代,可以把所有数据都过滤出来.如下: ...
- 深度理解python中的元类
本文转自:(英文版)https://stackoverflow.com/questions/100003/what-is-a-metaclass-in-python (翻译版) http:// ...
- 一文搞懂Python迭代器和生成器
很多童鞋搞不懂python迭代器和生成器到底是什么?它们之间又有什么样的关系? 这篇文章就是要用最简单的方式让你理解Python迭代器和生成器! 1.迭代器和迭代过程 维基百科解释道: 在Python ...
- 完全理解 Python 迭代对象、迭代器、生成器(转)
完全理解 Python 迭代对象.迭代器.生成器 本文源自RQ作者的一篇博文,原文是Iterables vs. Iterators vs. Generators » nvie.com,俺写的这篇文章是 ...
- 完全理解 Python 迭代对象、迭代器、生成器
完全理解 Python 迭代对象.迭代器.生成器 2017/05/29 · 基础知识 · 9 评论 · 可迭代对象, 生成器, 迭代器 分享到: 原文出处: liuzhijun 本文源自RQ作者 ...
- 大数据学习day15----第三阶段----scala03--------1.函数(“_”的使用, 函数和方法的区别)2. 数组和集合常用的方法(迭代器,并行集合) 3. 深度理解函数 4 练习(用java实现类似Scala函数式编程的功能(不能使用Lambda表达式))
1. 函数 函数就是一个非常灵活的运算逻辑,可以灵活的将函数传入方法中,前提是方法中接收的是类型一致的函数类型 函数式编程的好处:想要做什么就调用相应的方法(fliter.map.groupBy.so ...
- Python迭代器,生成器--精华中的精华
1. 迭代器 迭代器是访问集合元素的一种方式.迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束.迭代器只能往前不会后退,不过这也没什么,因为人们很少在迭代途中往后退.另外,迭代器的一大 ...
- [深度分析] Python Web 开发框架 Bottle
[深度分析] Python Web 开发框架 Bottle(这个真的他妈的经典!!!) 作者:lhf2009913 Bottle 是一个非常精致的WSGI框架,它提供了 Python Web开发中需要 ...
- python -迭代器与生成器 以及 iterable(可迭代对象)、yield语句
我刚开始学习编程没多久,对于很多知识还完全不知道,而有些知道的也是一知半解,我想把学习到的知识记录下来,一是弥补记忆力差的毛病,二也是为了待以后知识能进一步理解透彻时再回来做一个补充. 参考链接: 完 ...
随机推荐
- fork、exec 和 exit 对 IPC 对象的影响
GitHub: https://github.com/storagezhang Emai: debugzhang@163.com 华为云社区: https://bbs.huaweicloud.com/ ...
- mysql数据库忘记密码时如何修改密码
方法/步骤 1.进入 mysql 的 bin 目录下,打开 cmd ,在此之前关闭mysql服务,并且结束mysqld.exe进程 ...
- PAT (Advanced Level) Practice 1005 Spell It Right (20 分) 凌宸1642
PAT (Advanced Level) Practice 1005 Spell It Right (20 分) 凌宸1642 题目描述: Given a non-negative integer N ...
- [Fundamental of Power Electronics]-PART I-1.引言-1.2 1.3 电力电子技术的几个应用、本书内容
1.2 电力电子技术的几个应用 高效开关变换器面临的功率范围从 (1)小于1瓦(电池供电的便携式设备内的DC-DC转换器)到(2)计算机及办公设备中的几十,几百,数千瓦到(3)变速电机驱动器中上千瓦及 ...
- Java(195-214)【final、权限、内部类】
1.final关键字的概念与四种方法 今天是基础学习的最后一天!~ 2.final关键字用来修饰一个类 3.final关键字来修饰成员方法 4.final用于修饰局部变量 package cn.itc ...
- 解决Spring中使用Example无法查询到Mongodb中的数据问题
1 问题描述 在Spring Boot中使用Mongodb中的Example查询数据时查询不到,示例代码如下: ExampleMatcher matcher = ExampleMatcher.matc ...
- 18. VUE created 方法作用
一般可以在created函数中调用ajax获取页面初始化所需的数据. 实例的生命周期: 每个 Vue 实例在被创建之前都要经过一系列的初始化过程.例如,实例需要配置数据观测(data observer ...
- .Net之Docker部署详细流程
开篇语 自己从头开始走一遍docker部署.net的流程,作为一种学习总结,以及后续会写一些在该基础之上的文章. 本次示例环境:vs2019.net5.docker.postman 创建项目 本次事例 ...
- mac系统 PHP Nginx环境变量修改
场景:php默认的环境变量不是我们实际工作中想要的 执行命令:which php 查看默认的php指向的目录 : /usr/bin/php 修改· ~/.bash_profile 文件 添加php环 ...
- 【转】在CentOS 8 / RHEL 8上配置主/从BIND DNS服务器
转自: https://zh.codepre.com/centos-2700.html 前言 本指南描述了在CentOS 8 / RHEL 8 Linux上配置BIND DNS服务器所需的步骤.在Ce ...