python迭代器和可迭代对象
Posted 我家大宝最可爱
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了python迭代器和可迭代对象相关的知识,希望对你有一定的参考价值。
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__
这两个魔法函数即可,
- 调用迭代器对象的 __iter__方法得到还是迭代器对象本身,就跟没调用一样
- 调用迭代器对象的__next__方法返回下一个值,不依赖索引
- 可以一直调用__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
函数可以将自定义的迭代数据转换成迭代器
个人的一些见解
老实讲,我觉得迭代器还是比较鸡肋的,为了计算下一个状态,需要保存上下文,然后通过处理得到新的状态。如果我们想要实现的就是在循环中不断往下迭代,不需要之前的状态,那么可以使用迭
以上是关于python迭代器和可迭代对象的主要内容,如果未能解决你的问题,请参考以下文章