之前学习Python的时候,一直没有注意到迭代器这个东西。最近要使用到它,因此特来总结一下~~
这里不单独将__iter__
、__next__
、__getitem__
、next()
等概念拿出来将,只是内嵌到下面的内容中,感觉应该更好理解。
迭代的概念
Python中关于迭代有两个概念,第一个是Iterable,第二个是Iterator。
协议规定Iterable的__iter__
方法会返回一个迭代器对象(Iterator)。若Iterator含有__next__
方法(Python 2里是next)则该函数会返回下一个迭代值,如果迭代结束则抛出StopIteration异常;若该Iterator是通过__getitem__()
方法加iter()
函数得到的,则使用__getitem__()
返回下一个迭代值。
同时,Iterator自己也是一种Iterable,所以也需要实现Iterable的接口,也就是__iter__
或者__getitem__()
。
这均是英文的说法,感觉看起来花里胡哨的,而我们常用的是中文,下面介绍下中文的概念。注意这几个概念特别重要,要反复理解。
可迭代类(class collections.abc.Iterable)
- 提供
__iter__()
这个方法的类,都是可迭代类 - 提供
__getitem__()
这个方法的类,也是可迭代类
迭代器类(class collections.abc.Iterator)
同时提供 __iter__()
和 __next__()
这两个方法的类。从定义可以看出,
- 迭代器类,一定是可迭代类,因为它实现了
__iter__()
方法 - 从定义来看,迭代器类,要比可迭代类多实现一个
__next()__
方法
以上两个,在这个页面中可以找到:8.4. collections.abc - Abstract Base Classes for Containers - Python 3.6.3 documentation
以下两个,在这个页面中可以找到:Glossary - Python 3.6.3 documentation
可迭代对象
简单来说,就是那些 list, str, 和tuple 用这些定义的对象,都是 可迭代对象,因为他们都实现了__iter__()
或 __getitem__()
方法。(当然,也可以是你自己定义一个类生成的一个对象)。
迭代器对象
代表数据流的对象。传说中的迭代器。得到该对象有一下两种方式:
实例化迭代器类
提供了
__getitem__()
方法的可迭代类,并使用iter
函数作用于该类的实例。
相比较来说,迭代器对象一定是可迭代对象,而可迭代对象因为可能不含有__next__
函数,所以不一定可以使用next()
函数,但是可以使用iter()
函数将其转为迭代器对象,也就可以使用next()
函数。
迭代器协议
迭代器协议指的是容器类需要包含一个特殊方法。
如果一个容器类提供了 __iter__()
方法,并且该方法能返回一个能够逐个访问容器内所有元素的迭代器,则我们说该容器类实现了迭代器协议。
Python 中的迭代器协议和 Python 中的 for
循环是紧密相连的。
1 | # iterator protocol and for loop |
Python 处理 for
循环时,首先会调用内建函数 iter(something)
,它实际上会调用 something.__iter__()
,返回 something
对应的迭代器对象。而后,for
循环会调用内建函数 next()
,作用在迭代器上,获取迭代器的下一个元素,并赋值给 x
。此后,Python 才开始执行循环体。
当然若函数没有
__iter__
函数,只有__getitem__()
方法,照样可以for循环,此时就不是这套协议了。这里放一个迭代器协议是为了方便自己理解for循环是如何在含有__iter__()
方法的可迭代类中工作的。
例子
例一
例如下面的代码:
1 | class IterDemo(object): |
输出结果为:
1 | __next__ : 1 |
当我们实现了__iter__
以及__next__
方法的时候,使用for循环调用__iter__
方法获取Iterable的对象,然后调用该对象的next方法。
当我们使用iter()
函数的时候,会调用__iter__
函数,返回的是该类自身,因此可以使用iter(it).count
打印成员变量。
另外,我们在使用for
语句的时候,Python内部其实是把for
后面的对象上使用了内建函数iter
,比如:
1 | a = [1, 2, 3] |
其实在Python内部进行了类似如下的转换:
1 | a = [1, 2, 3] |
例二
当我们去掉__iter__
方法的时候,代码如下:
1 | class IterDemo(object): |
此时运行代码会报错,提示TypeError: 'IterDemo' object is not iterable
。这是因为我们虽然实现了__next__
用法,但是没有实现__iter__
方法,返回的结果只是Iterator,而不是iterable。因此当我们将主函数改为如下代码:
1 | if __name__ == "__main__": |
运行结果为:'__next__ : 1'
。因为它实现了__next__
方法,因此可以使用next()
方法。
例三
当没有实现__iter__
方法时,却只实现了__getitem__
方法时,代码如下:
1 | class IterDemo(object): |
输出结果为:
1 | 0 |
可以看到,此时for循环会调用__getitem__
,会改用下标迭代的方式。而next()
方法会调用__next__
函数。
例四
代码如下:
1 | class Iter(object): |
运行结果为:
1 | 0 |
可以看到可以使用for循环访问的均是可迭代对象,不一定要求是迭代器对象。至于for循环是访问什么元素,若类中有__iter__
函数,则看该函数返回的是哪一个迭代器对象,若没有__iter__
但是含有__getitem__()
,则看该函数实现的具体功能。
深入理解迭代
Python中许多方法直接返回iterator,比如itertools里面的izip等方法,如果Iterator自己不是Iterable的话,就很不方便,需要先返回一个Iterable对象,再让Iterable返回Iterator。生成器表达式也是一个iterator,显然对于生成器表达式直接使用for是非常重要的。
那么为什么不只保留Iterator的接口而还需要设计Iterable呢?许多对象比如list、dict,是可以重复遍历的,甚至可以同时并发地进行遍历,通过__iter__
每次返回一个独立的迭代器,就可以保证不同的迭代过程不会互相影响。而生成器表达式之类的结果往往是一次性的,不可以重复遍历,所以直接返回一个Iterator就好。让Iterator也实现Iterable的兼容就可以很灵活地选择返回哪一种。
总结来说Iterator实现的__iter__
是为了兼容Iterable的接口,从而让Iterator成为Iterable的一种实现。
for为了兼容性其实有两种机制,如果对象有__iter__
会使用迭代器调用__next__
,但是如果对象没有__iter__
,但是实现了__getitem__
,会改用下标迭代的方式。我们可以试一下:
1 | class NotIterable(object): |
当for发现没有__iter__
但是有__getitem__
的时候,会从0开始依次读取相应的下标,直到发生IndexError为止,这是一种旧的迭代协议。iter方法也会处理这种情况,在不存在__iter__
的时候,返回一个下标迭代的iterator对象来代替。一个重要的例子是str,字符串就是没有__iter__
接口的。
判断Iterable和Iterator
可以使用isinstance()
判断一个对象是否是Iterable
对象:
1 | from collections import Iterable |
可以使用isinstance()
判断一个对象是否是Iterator
对象:
1 | from collections import Iterator |
关于Iterable和Iterator,我们有如下几个结论:
凡是可作用于
next()
函数的对象都是Iterator
类型,它们表示一个惰性计算的序列;集合数据类型如
list
、dict
、str
等是Iterable
但不是Iterator
,不过可以通过iter()
函数获得一个Iterator
对象。
iter()函数
Python 3中关于iter(object[, sentinel)]
方法有两个参数。使用iter(object)
这种形式比较常见。iter(object, sentinel)
这种形式一般较少使用。下面着重介绍下iter(object)
这种形式
此时,参数object必须是集合对象,且支持迭代协议(iteration protocol)或者支持序列协议(sequence protocol)。也就是实现了__iter__()
方法或者__getitem__()
方法,说白了就是可迭代类。
iter()
函数返回值为迭代器对象,可以使用next()
函数。若iter()
函数作用的对象是实现了__iter__
方法的可迭代类,则该可迭代类中__iter__
方法返回的迭代器对象即为iter()
函数返回的迭代器对象,此时的next()
函数或者for循环会依次访问该迭代器对象中的每一个元素;若iter()
函数作用的对象是实现了__getitem__
方法的可迭代类,则会返回一个iterator
对象,此时的next()
函数或者for循环会依次按下标传参给__getitem__
方法得到每一个返回值。
例一
iter()
函数作用于实现了__getitem__()
方法的可迭代类,结果如下:
1 | class IterDemo(object): |
因为这个类中没有__next__
函数,而有__getitem_
函数,所以是可迭代类,不是迭代器类,因此本来不能使用next()
用法(但是可以使用for循环按下标访问),但是我们使用iter()
函数可以将转换为Iterator
类型,也就可以使用next()
用法了,且调用了__getitem_
方法,以下标的形式访问每一个元素。
输出结果:
1 | <iterator at 0x7fa28c28b0f0> |
值得注意的是,不能直接将next和iter放在一起使用,例如print(next(iter(it)))
,否则的话每次输出的结果均为0,具体原因未知。
例二
iter()
函数作用于实现了__iter__()
方法的可迭代类,结果如下:
1 | class SequentialSampler(): |
输出结果为:
1 | <range_iterator object at 0x7fa28c31e600> |
可以看到,此时SequentialSampler
类为可迭代类但不是迭代器类,可以使用for循环访问__iter__
函数返回的迭代器对象中的每一个元素,但是不能使用next()
元素访问。当使用iter()
函数作用于SequentialSampler
的it
对象的时候,返回的结果为range_iterator
,也就是将可迭代类中__iter__
方法返回的迭代器对象即为iter()
函数返回的迭代器对象。此时就可以使用for循环或者next()
函数访问iter()
函数返回的迭代器对象中的每一个元素了。
总结
- 迭代器类(
__iter__()
和__next__()
)一定是可迭代类(__iter__()
或者__getitem__()
);因此迭代器对象(实例化迭代器类,或者iter()
函数作用于可迭代对象)一定是可迭代对象,而迭代器对象即为平时所说的迭代器。 - 可以使用for循环访问的均是可迭代对象,不一定要求是迭代器对象。至于for循环是访问什么元素,若类中有
__iter__
函数,则看该函数返回的是哪一个迭代器对象,根据该迭代器对象是怎么来的,来看具体是使用该迭代器对象中的__next__
方法还是__getitem__()
方法返回for循环的每一个元素;若没有__iter__
但是含有__getitem__()
,则根据该函数按下标返回for循环的每一个元素。 - 可以使用
next()
函数访问的只有迭代器对象。有两种情况,一是实现了__next__
方法的迭代器类的实例对象;二是iter()
函数作用于可迭代对象。 - 集合数据类型如
list
、dict
、str
等是可迭代类(Iterable)但不是迭代器类(Iterator),不过可以通过iter()
函数作用于其实例获得一个迭代器对象(Iterator)。 - 使用
next()
访问元素和使用for循环访问元素的区别在于,使用前者当没有元素的时候会抛出异常,而后者不会。 iter()
函数作用于可迭代对象,返回的是迭代器对象。若iter()
函数作用的对象是实现了__iter__
方法的可迭代类,则该可迭代类中__iter__
方法返回的迭代器对象即为iter()
函数返回的迭代器对象,此时的next()
函数或者for循环会依次访问该迭代器对象中的每一个元素;若iter()
函数作用的对象是实现了__getitem__
方法的可迭代类,则会返回一个iterator
对象,此时的next()
函数或者for循环会依次按下标传参给__getitem__
方法得到每一个返回值。
参考
python的迭代器为什么一定要实现iter方法? - 灵剑的回答 - 知乎
Python随笔之iter,iter,next,next,generator之间的关系
python iter函数用法
迭代器
【python魔术方法】迭代器(iter和next)
python的迭代器为什么一定要实现iter方法? - asialine的回答 - 知乎
Python 中的黑暗角落(一):理解 yield 关键字