python迭代器和可迭代对象

2023-10-26

1. 迭代器 vs 可迭代对象

python中两个迭代的概念,一个叫做迭代器(Iterator),一个叫做可迭代对象(Iterable),我们可以从collections模块中导入

from collections.abc import Iterable,Iterator

当我们实现了迭代器之后,就可以使用for循环进行遍历了。我们平常使用的字符串,列表,元组和字典等,底层都实现了迭代器。我们可以通过instance来判断

from collections.abc import Iterable,Iterator


s = "abcdefgh"
print(isinstance(s,Iterable)) # True
print(isinstance(s,Iterator)) # False

l = [1,2,3,4,5,6,7,8]
print(isinstance(s,Iterable)) # True
print(isinstance(s,Iterator)) # False

t = (1,2,3,4,5,6,7,8)
print(isinstance(s,Iterable)) # True
print(isinstance(s,Iterator)) # False

哈哈,上来就打脸了,发现字符串,列表和元组并不是迭代器,而是迭代对象。不要紧,继续往下看

2. 如何实现迭代器

迭代器的实现非常简单,只需要实现__iter____next__这两个魔法函数即可,

  1. 调用迭代器对象的 __iter__方法得到还是迭代器对象本身,就跟没调用一样
  2. 调用迭代器对象的__next__方法返回下一个值,不依赖索引
  3. 可以一直调用__next__直到取干净,最后抛出异常StopIteration(停止迭代)

我们来实现一个斐波那契数列

from collections.abc import Iterable,Iterator

class Fib:
    def __init__(self):
        self.prev = 0
        self.curr = 1
    
    def __iter__(self): # 自身就是迭代器,所以返回自身
        return self
    
    def __next__(self): # 只有实现了__next__函数才是迭代器
    	print('run __next__ func')
        self.prev,self.curr = self.curr,self.curr+self.prev
        return self.curr

fib = Fib()
print(isinstance(fib,Iterator)) # True
print(isinstance(fib,Iterable)) # True
# 通过next函数获取下一个值
print(next(fib)) # 1
print(next(fib)) # 2
print(next(fib)) # 3
print(next(fib)) # 5

索然无味啊,这迭代器好像并没有什么特别的地方。每次调用next函数,就会进入到__next__函数中,然后计算curr的值,返回出来。

上面实现的迭代器是没有终止条件的,只要你愿意就可以一直计算下去,但是一般的迭代器都是有终止条件的,我们修改一下上面的迭代器

from collections.abc import Iterable,Iterator
from operator import index

class Fib:
    def __init__(self, end):
        self.prev = 0
        self.curr = 1
        self.end = end
        self.index = 0
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.index < self.end:
            self.prev,self.curr = self.curr,self.curr+self.prev
            self.index += 1
            return self.curr
        else:
            raise StopIteration

fib = Fib(3)

print(next(fib)) # 1
print(next(fib)) # 2
print(next(fib)) # 3

可以看到正常输出了,如果我们再次调用next呢?

next(fib)

Traceback (most recent call last):
File “fib.py”, line 26, in
print(next(fib)) # 5
File “fib.py”, line 19, in next
raise StopIteration
StopIteration

可以看到,我们得到一个异常对象,注意,主动抛出来的是异常对象,而不是异常,说明我们访问结束了。for循环帮我们处理了异常

for v in fib:
    print(v)

for循环实现的逻辑是这样的

# create an iterator object from that iterable
iter_obj = iter(iterable)
 
# infinite loop
while True:
	try:
		# get the next item
		element = next(iter_obj)
		print(element)
		# do something with element
	except StopIteration:
		# if StopIteration is raised, break from loop
	break

就很有意思,捕获的是StopIteration,那如果我抛出的不是这个异常呢,比如我抛出一个IndexError


import time

class Fib:
    def __init__(self, end):
        self.prev = 0
        self.curr = 1
        self.end = end
        self.index = 0
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.index < self.end:
            self.prev,self.curr = self.curr,self.curr+self.prev
            self.index += 1
            time.sleep(0.1)
            print('.',end='',flush=True)
            return self.curr
        else:
            raise IndexError('index > end')

fib = Fib(10)
for v in fib:
    print(v)

Traceback (most recent call last):
File “fib.py”, line 20, in
for v in fib:
File “fib.py”, line 17, in next
raise IndexError(‘index > end’)
IndexError: index > end

可以看到for循环并没有捕获这个异常,而是直接抛出来了。后来我一寻思,觉得理应如此,for循环之需要判断是否完成遍历即可,其他的异常应该交给用户自己处理。

3. 迭代器的好处

回想一下我们平时是怎么实现斐波那契数列的

3.1 每次从头计算(空间换时间)

这种方式非常的耗时,每次都要重头开始计算,但好处是想要获取哪一位的数值直接调用即可,没有任何约束和依赖

import time
def fib(end):
    i = 0
    prev, curr = 0, 1
    print('calc {}th '.format(end),end='')
    while i < end:
        prev, curr = curr, prev + curr
        i += 1
        time.sleep(0.1)
        print('.',end='',flush=True)
    return curr
start = time.perf_counter()
for i in range(10):
	print(fib(i))
end = time.perf_counter()
print('fib cost time {}'.format(end-start))

3.2 结果保存下来(时间换空间)

相对于从头计算,我们可以将所有的结果保存下来,这种方式只需要计算一次,就可以获取之前任意一次的结果,因为所有结果都保存下来了,所以很占空间。

import time
def fib(end):
    print('calc {} '.format(end),end='')
    res = [1]
    i = 0
    prev, curr = 0, 1
    while i < end:
        prev, curr = curr, prev + curr
        i += 1
        res.append(curr)
        time.sleep(0.1)
        print('.',end='',flush=True)
    print('\n')
    return res

start = time.perf_counter()
res = fib(10)
for i in range(10):
	print(res[i])
end = time.perf_counter()
print('fib cost time {}'.format(end-start))

3.3 迭代器实现

迭代器的实现前面已经说过了,很有意思的一点是,我们只有调用了next函数,迭代器才会返回我们下一个结果。如果我想要获取第5个斐波那契数列值,我需要调用5次next函数,每调用一次就会执行一次__next__函数。

import time

class Fib:
    def __init__(self, end):
        self.prev = 0
        self.curr = 1
        self.end = end
        self.index = 0
    def __iter__(self):
        return self
    
    def __next__(self):
        print('calc',end='')
        if self.index < self.end:
            self.prev,self.curr = self.curr,self.curr+self.prev
            self.index += 1
            time.sleep(0.1)
            print('.',end='',flush=True)
            return self.curr
        else:
            raise StopIteration
start = time.perf_counter()
fib = Fib(10)
for v in fib:
    print(v)
end = time.perf_counter()
print('fib cost time {}'.format(end-start))

4 迭代器的局限

迭代器不基于索引的方式获取可迭代对象中的元素。节省了大量的内存,迭代器在内存中相当于只占一个数据的空间。因为每次取值都上一条数据会在内存释放,加载当前的此条数据。惰性机制非常有用,我们平时处理大文件的时候,没有办法一下子读到内存中来,就可以通过迭代器的方式一条一条的读取。
迭代器取值时不走回头路,只能一直向下取值,所以不能直观的查看里面的数据。老实讲,我觉得索引的方式非常的好,想要获取那个位置的数据,直接就可以获取,历史所有的状态都保留了下来,而迭代器则需要一步一步计算过去。所以说,迭代器是一个双刃剑,就看你想要在什么场景下使用

可迭代对象

可迭代对象就非常简单了,迭代器需要实现两个函数,一个是__next__用来计算下一个值,一个是__iter__返回自身,因为自身就是迭代器。可迭代对象只需要实现一个__iter__函数就可以了,这个函数返回一个迭代器。

from collections.abc import Iterable,Iterator
from operator import index

class Fib:
    def __init__(self, end):
        self.prev = 0
        self.curr = 1
        self.end = end
        self.index = 0
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.index < self.end:
            self.prev,self.curr = self.curr,self.curr+self.prev
            self.index += 1
            return self.curr
        else:
            raise StopIteration


class Fibable:
    def __init__(self, end):
        self.end = end
    
    def __iter__(self):
        return Fib(self.end)

fib = Fibable(10)
print(isinstance(fib,Iterable)) # True 是可迭代对象
print(isinstance(fib,Iterator)) # False 不是迭代器
for i in fib:
    print(i)

但是可迭代对象没有实现__next__函数,因此无法通过next来获取下一个元素,只能通过for循环获取数据,如果调用next函数则会报错,而且可以看到,报的是类型错误,next函数只能接受迭代器。

next(fib)

Traceback (most recent call last):
File “fib.py”, line 38, in
next(fib)
TypeError: ‘Fibable’ object is not an iterator

自定义可迭代数据

python中还提供了一个魔法函数__getitem__,实现这个函数之后,就可以使用for循环遍历了,pytorch中的dataloader就是这种方式,先举个简单的例子。

from collections.abc import Iterable,Iterator

class Fib:
    def __init__(self) -> None:
        self.res = [0,1,1,2,3,5,8,13,21,35,56,91]

    def __getitem__(self,index):
    	print('run fib func...')
        return self.res[index]

fib = Fib()
print(isinstance(fib,Iterable)) # False 不是可迭代对象
print(isinstance(fib,Iterator)) # False 不是迭代器
for v in fib: # 啥也不是,但就是可以for循环
    print(v)

Fib既不是可迭代对象,也不是迭代器,但是可以使用for循环来遍历数据。而且发现__getitem__还有一个入参index,这说明我们其实可以直接通过索引遍历数据。看看斐波那契数列如何实现

from collections.abc import Iterable,Iterator

class Fib:
    def __init__(self) -> None:
        pass

    def __getitem__(self,index):
        i = 0
        prev, curr = 0, 1
        while i < index:
            print('calc fib...')
            prev, curr = curr, prev + curr
            i += 1
        return curr

fib = Fib()
print(isinstance(fib,Iterable)) # False 不是可迭代对象
print(isinstance(fib,Iterator)) # False 不是迭代器

print(fib[3])

上面实现了斐波那契数列,我们没有保存结果,每次读取结果,每次重头开始计算。简直是无话可说,这就相当于第一种最耗时的方案,非常的愚蠢。我可以稍作修改,变成第二种方式,把所有的结果再初始化的时候都计算一遍然后保存下来,而且可以随意索引,就是占空间。

from collections.abc import Iterable,Iterator

class Fib:
    def __init__(self, end):
    	# 初始化的时候将所有的结果计算一遍,然后保存下来
        self.res = []
        i = 0
        prev, curr = 0, 1
        while i < end:
            prev, curr = curr, prev + curr
            i += 1
            self.res.append(curr)

    def __getitem__(self,index):
        return self.res[index]

fib = Fib(10)
print(isinstance(fib,Iterable)) # False 不是可迭代对象
print(isinstance(fib,Iterator)) # False 不是迭代器

# 可以使用for循环
for v in fib:
    print(v)

print(fib[3]) # 可以通过索引随意取值

其实我觉得这就是一个单纯的get方法而已,因为其实还有一个方法,叫做__setitem__,这不就是平时使用的@pro@name.setter吗?
python提供了iter函数,可以将__getitem__转变称迭代器

fib = Fib(10)
print(isinstance(fib,Iterable)) # False 不是可迭代对象
print(isinstance(fib,Iterator)) # False 不是迭代器

fib_iter = iter(fib)
print(isinstance(fib_iter,Iterable)) # True 是可迭代对象
print(isinstance(fib_iter,Iterator)) # True 是迭代器

胡乱测试

可迭代对象只实现了__iter__,如果我再实现了__next__会不会变成迭代器

from collections.abc import Iterable,Iterator
from operator import index

class Fib:
    def __init__(self, end):
        self.prev = 0
        self.curr = 1
        self.end = end
        self.index = 0
    def __iter__(self): # 本身就是迭代器,直接返回自己
        return self
    
    def __next__(self):
        if self.index < self.end:
            self.prev,self.curr = self.curr,self.curr+self.prev
            self.index += 1
            return self.curr
        else:
            raise StopIteration


class Fibable:
    def __init__(self, end):
        self.end = end
    
    def __iter__(self): # 要求返回一个迭代器
        return Fib(self.end)

    def __next__(self):
        print('next func ...')

fib = Fibable(10)
print(isinstance(fib,Iterable)) # True 可迭代对象
print(isinstance(fib,Iterator)) # True 不是迭代器
for i in fib:
    print(i)

万万没想到,竟然真的变成迭代器了,而且可以正常打印出结果。这说明,在迭代的过程中并没有调用__next__,而是调用了__iter__函数。这说明如果同时存在的话,会优先调用__iter__。再尝试使用next执行看看

next(fib) # next func ...
next(fib) # next func ...
next(fib) # next func ...

输出的是__next__的结果,而不是__iter__的结果。

如果同时存在__getitem____next__会怎么样呢?

from collections.abc import Iterable,Iterator
from operator import index

class Fib:
    def __init__(self):
        pass

    def __iter__(self):
        return self
    
    def __next__(self):
        print('next func')

    def __getitem__(self,index):
        print('getitem func {}'.format(index))


fib = Fib()
print(isinstance(fib,Iterable)) # True 是可迭代对象
print(isinstance(fib,Iterator)) # True 是迭代器
i = 0
for v in fib:
    if i > 10:break
    i += 1

next(fib)
next(fib)
next(fib)

fib[3]

for会发现优先调用__next__函数,使用next函数调用__next__函数,使用索引调用__getitem__函数

总结

  • 迭代器:实现__iter____next__两个魔法函数,可以使用for循环和next
  • 可迭代对象:只能实现__iter__函数,并且这个函数返回的是一个迭代器,可以使用for,由于没有实现__next__函数,所以不能使用next
  • 自定义可迭代数据:实现__getitem__函数,有一个入参index,意味着我们可以通过索引访问,所以也就意味着需要保存较多的数据在内存中,通过iter函数可以将自定义的迭代数据转换成迭代器

个人的一些见解
老实讲,我觉得迭代器还是比较鸡肋的,为了计算下一个状态,需要保存上下文,然后通过处理得到新的状态。如果我们想要实现的就是在循环中不断往下迭代,不需要之前的状态,那么可以使用迭代器,如果想要通过索引来快速得到想要的结果,最好还是使用自定义的迭代对象。

应用举例

我能想到使用迭代器的场景,一般会满足两个条件

  1. 数据集非常的大,无法一下子加载到内存中
  2. 顺序的消耗数据,一般不会有状态的跳跃

例如我们想要处理1T的数据,不会直接加载到内存中,而是一条一条的加载,这个时候我们就可以把所有的路径加载进来,然后读取每个路径的文件,迭代的对这些数据进行处理。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

python迭代器和可迭代对象 的相关文章

随机推荐

  • MySQL优化(二):MySQL 索引深入解读

    目录 一 索引是什么 1 索引定义 2 索引类型 3 索引的创建 4 索引的删除 二 索引存储模型 2 1 二分查找 2 2 二叉查找树 2 3 平衡二叉树 2 4 多路平衡查找树 B Tree 2 5 加强版多路平衡查找树 B Tree
  • 【统计模拟及其R实现】分层抽样法 / 条件期望法 习题答案(超详细)

    课本 统计模拟及其R实现 肖枝红 朱强 武汉大学出版社 参考资料 方差缩减技术 条件期望法 目录 1 分层抽样法 2 条件期望法 1 分层抽样法 题目1 如何通过分层抽样法得到
  • [人工智能-深度学习-51]:循环神经网络 - RNN基本原理详解

    作者主页 文火冰糖的硅基工坊 文火冰糖 王文兵 的博客 文火冰糖的硅基工坊 CSDN博客 本文网址 https blog csdn net HiWangWenBing article details 121387285 目录 第1章 详解前
  • gmapping 分析

    转载 这一篇先讲我对gmapping源码的理解 难免有错 欢迎指正 相互学习 原博客 https blog csdn net roadseek zw article details 53316177 博客主页 https blog csdn
  • [k8s部署踩过的坑]

    系统环境 系统版本 docker版本 role ip地址 CentOS8 4 2105 Linux version 4 18 0 348 xx Red Hat 8 5 0 4 20 10 12 k8s master 192 168 100
  • 冒泡排序详解

    一 冒泡排序简介 常用排序算法 冒泡排序 Bubble Sort 是一种常见的排序算法 相对来说比较简单 冒泡排序重复地走访需要排序的元素列表 依次比较两个相邻的元素 如果顺序 如从大到小或从小到大 错误就交换它们的位置 重复地进行直到没有
  • python海龟漂亮图案代码大全_带有海龟图案的Python花

    我在高中的编程课上和海龟图形一起工作 这个项目是按照老师演示的一些指导原则和功能制作一朵花 我在一个小时内就完成了 现在我正试图用更多的海龟一次画出多朵花 但我不能让海龟们使用新定义的函数 老师也没有时间和我一对一地讨论我该怎么做 所以 经
  • Cesium:入门教程(二)之数据源加载

    前言 成功运行 helloworld 的例子后 下面对控件 数据源等进一步说明 鼠标 左键单击和拖拽 沿着地球表面平移 调整相机位置 右键单击和拖拽 相机放大缩小 调整相机距离 滚轮 相机放大缩小 调整相机距离 中间按下和拖拽 围绕地球表面
  • Git命令介绍

    1 最小配置 在使用Git之前需要配置User信息 包括user name和user email git config global user name your name git config global user email your
  • openGL之API学习(八十二)glShaderSource

    替换着色器中的代码 任何以前的代码都会被完全替换掉 一次可以上传多段代码进行替换 并不进行代码的扫描和解析 替换完后是否需要重新进行编译和链接呢 因为着色器代码需要编译 连接 最后生成可执行文件才能被CPU GPU调度执行 所以替换完后还是
  • EasyImage简单图床 - 快速搭建私人图床云盘同时远程访问

    文章目录 1 前言 2 EasyImage网站搭建 2 1 EasyImage下载和安装 2 2 EasyImage网页测试 2 3 cpolar的安装和注册 3 本地网页发布 3 1 Cpolar云端设置 3 2 Cpolar内网穿透本地
  • Maven插件之Dependency:analyze

    前言 完成新功能的开发后 在发包前组长告诉我要检查maven工程的依赖 并告诉我相关指令 此文记录一下使用方式 正文 简介 Maven官网之Dependency插件 Dependency插件提供了操纵artifact的能力 可以复制以及拆包
  • Loadrunner手写接口性能脚本

    Loadrunner手写接口性能脚本 文章目录 概述 脚本录制出现的问题 手写loadrunner脚本 概述 使用Loadrunner进行性能测试分为三步 1 创建 编辑脚本 2 运行负载测试 3 分析测试结果 脚本录制出现的问题 1 录制
  • mybatis如何防止SQL注入?

    sql注入发生的时间 sql注入发生的阶段在sql预编译阶段 当编译完成的sql不会产生sql注入 一 采用jdbc操作数据时候 String sql update ft proposal set id id PreparedStateme
  • 【rust/egui】(十一)使用rfd选择文件并使用serde_json进行序列化

    说在前面 rust新手 egui没啥找到啥教程 这里自己记录下学习过程 环境 windows11 22H2 rust版本 rustc 1 71 1 egui版本 0 22 0 eframe版本 0 22 0 上一篇 这里 rfd Rusty
  • 学习笔记之以太网帧结构

    在TCP IP中 以太网的IP数据报文的封装格式由RFC 894定义 IEEE802 3网络的IP数据报文封装由RFC 1042定义 当今最常使用的封装格式是RFC894定义的格式 通常称为Ethernet II或者Ethernet DIX
  • openGL之API学习(一零零)glProgramParameter

    给着色器程序传递参数 void glProgramParameteri GLuint program GLenum pname GLint value program Specifies the name of a program obje
  • python 散点图_

    Python中绘制散点图常用的函数是 matplotlib pyplot scatter 它的主要参数如下 matplotlib pyplot scatter x y s None c None marker None cmap None
  • 【SpringBoot】1、SpringBoot整合JWT实现Token验证

    这里写目录标题 1 单点登录 1 1 单系统登录 1 1 1 单系统登录流程 使用Session实现单系统登录 1 2 多系统 单点 登录 1 2 1 单点登录实现方案 1 2 1 1 Session跨域 1 2 1 2 Spring Se
  • python迭代器和可迭代对象

    1 迭代器 vs 可迭代对象 python中两个迭代的概念 一个叫做迭代器 Iterator 一个叫做可迭代对象 Iterable 我们可以从collections模块中导入 from collections abc import Iter