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)

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的数据,不会直接加载到内存中,而是一条一条的加载,这个时候我们就可以把所有的路径加载进来,然后读取每个路径的文件,迭代的对这些数据进行处理。

以上是关于python迭代器和可迭代对象的主要内容,如果未能解决你的问题,请参考以下文章

python迭代器和可迭代对象

python的生成器与迭代器和可迭代对象

迭代器和可迭代协议

Python基础-16生成器-迭代器

Python基础-16生成器-迭代器

理解迭代器和可迭代对象