Python迭代器

之前学习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
2
3
# iterator protocol and for loop
for x in something:
print(x)

Python 处理 for 循环时,首先会调用内建函数 iter(something),它实际上会调用 something.__iter__(),返回 something 对应的迭代器对象。而后,for 循环会调用内建函数 next(),作用在迭代器上,获取迭代器的下一个元素,并赋值给 x。此后,Python 才开始执行循环体。

当然若函数没有__iter__函数,只有__getitem__()方法,照样可以for循环,此时就不是这套协议了。这里放一个迭代器协议是为了方便自己理解for循环是如何在含有__iter__() 方法的可迭代类中工作的。

例子

例一

例如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class IterDemo(object):

def __init__(self):
self.count = 0

def __iter__(self):
# 该函数会由iter函数调用 返回一个可以迭代的对象
return self

def __next__(self):
# 该函数会由next内建函数调用
if self.count < 10:
self.count += 1
return "__next__ : %s" % self.count
else:
raise StopIteration


if __name__ == "__main__":
it = IterDemo()

for i in iter(it):
print(i)

print(iter(it))
print(iter(it).count)

输出结果为:

1
2
3
4
5
6
7
8
9
10
11
12
__next__ : 1
__next__ : 2
__next__ : 3
__next__ : 4
__next__ : 5
__next__ : 6
__next__ : 7
__next__ : 8
__next__ : 9
__next__ : 10
<__main__.IterDemo at 0x7fa4b4079f98>
10

当我们实现了__iter__以及__next__方法的时候,使用for循环调用__iter__方法获取Iterable的对象,然后调用该对象的next方法。

当我们使用iter()函数的时候,会调用__iter__函数,返回的是该类自身,因此可以使用iter(it).count打印成员变量。

另外,我们在使用for语句的时候,Python内部其实是把for后面的对象上使用了内建函数iter,比如:

1
2
3
a = [1, 2, 3]
for i in a:
do_something()

其实在Python内部进行了类似如下的转换:

1
2
3
a = [1, 2, 3]
for i in iter(a):
do_something()

例二

当我们去掉__iter__方法的时候,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class IterDemo(object):

def __init__(self):
self.count = 0

def __next__(self):
# 该函数会由next内建函数调用
if self.count < 10:
self.count += 1
return "__next__ : %s" % self.count
else:
raise StopIteration


if __name__ == "__main__":
it = IterDemo()

for i in it:
print(i)

此时运行代码会报错,提示TypeError: 'IterDemo' object is not iterable。这是因为我们虽然实现了__next__用法,但是没有实现__iter__方法,返回的结果只是Iterator,而不是iterable。因此当我们将主函数改为如下代码:

1
2
3
if __name__ == "__main__":
it = IterDemo()
print(next(it))

运行结果为:'__next__ : 1'。因为它实现了__next__方法,因此可以使用next()方法。

例三

当没有实现__iter__方法时,却只实现了__getitem__方法时,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class IterDemo(object):

def __init__(self):
self.count1 = 0
self.count = [x for x in range(10)]

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

def __next__(self):
# 该函数会由next内建函数调用
if self.count1 < 10:
self.count1 += 1
return "__next__ : %s" % self.count1
else:
raise StopIteration


if __name__ == "__main__":
it = IterDemo()
for i in it:
print(i)
print(next(it))

输出结果为:

1
2
3
4
5
6
7
8
9
10
11
0
1
2
3
4
5
6
7
8
9
__next__ : 1

可以看到,此时for循环会调用__getitem__,会改用下标迭代的方式。而next()方法会调用__next__函数。

例四

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Iter(object):

def __init__(self):
self.count = [x for x in range(3)]

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


class IterDemo(object):

def __init__(self):
self.count = [x for x in range(10)]

def __iter__(self):
return iter(Iter())

if __name__ == "__main__":
it = IterDemo()
for i in it:
print(i)

运行结果为:

1
2
3
0
1
2

可以看到可以使用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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> class NotIterable(object):
... def __init__(self, baselist):
... self._baselist = baselist
... def __getitem__(self, index):
... return self._baselist[index]
...
>>> t = NotIterable([1,2,3])
>>> for i in t:
... print i
...
1
2
3
>>> iter(t)
<iterator object at 0x0345E3D0>

当for发现没有__iter__但是有__getitem__的时候,会从0开始依次读取相应的下标,直到发生IndexError为止,这是一种旧的迭代协议。iter方法也会处理这种情况,在不存在__iter__的时候,返回一个下标迭代的iterator对象来代替。一个重要的例子是str,字符串就是没有__iter__接口的。

判断Iterable和Iterator

可以使用isinstance()判断一个对象是否是Iterable对象:

1
2
3
4
5
6
7
8
9
10
11
>>> from collections import Iterable
>>> isinstance([], Iterable)
True
>>> isinstance({}, Iterable)
True
>>> isinstance('abc', Iterable)
True
>>> isinstance((x for x in range(10)), Iterable)
True
>>> isinstance(100, Iterable)
False

可以使用isinstance()判断一个对象是否是Iterator对象:

1
2
3
4
5
6
7
8
9
>>> from collections import Iterator
>>> isinstance((x for x in range(10)), Iterator)
True
>>> isinstance([], Iterator)
False
>>> isinstance({}, Iterator)
False
>>> isinstance('abc', Iterator)
False

关于Iterable和Iterator,我们有如下几个结论:

  • 凡是可作用于next()函数的对象都是Iterator类型,它们表示一个惰性计算的序列;

  • 集合数据类型如listdictstr等是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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class IterDemo(object):

def __init__(self):
self.count = [x for x in range(10)]

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


if __name__ == "__main__":
it = IterDemo()
it = iter(it)
print(it)
print(next(it))
print(next(it))

因为这个类中没有__next__函数,而有__getitem_函数,所以是可迭代类,不是迭代器类,因此本来不能使用next()用法(但是可以使用for循环按下标访问),但是我们使用iter()函数可以将转换为Iterator类型,也就可以使用next()用法了,且调用了__getitem_方法,以下标的形式访问每一个元素。

输出结果:

1
2
3
<iterator at 0x7fa28c28b0f0>
0
1

值得注意的是,不能直接将next和iter放在一起使用,例如print(next(iter(it))),否则的话每次输出的结果均为0,具体原因未知。

例二

iter()函数作用于实现了__iter__()方法的可迭代类,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class SequentialSampler():
r"""Samples elements sequentially, always in the same order.
Arguments:
data_source (Dataset): dataset to sample from
"""

def __init__(self, data_source):
self.data_source = data_source

def __iter__(self):
return iter(range(len(self.data_source)))

def __len__(self):
return len(self.data_source)

if __name__ == "__main__":
it = SequentialSampler(range(10))
it = iter(it)
print(it)
print(next(it))
print(next(it))

输出结果为:

1
2
3
<range_iterator object at 0x7fa28c31e600>
0
1

可以看到,此时SequentialSampler类为可迭代类但不是迭代器类,可以使用for循环访问__iter__函数返回的迭代器对象中的每一个元素,但是不能使用next()元素访问。当使用iter()函数作用于SequentialSamplerit对象的时候,返回的结果为range_iterator,也就是将可迭代类中__iter__方法返回的迭代器对象即为iter()函数返回的迭代器对象。此时就可以使用for循环或者next()函数访问iter()函数返回的迭代器对象中的每一个元素了。

总结

  • 迭代器类( __iter__()__next__())一定是可迭代类(__iter__()或者__getitem__() );因此迭代器对象(实例化迭代器类,或者iter()函数作用于可迭代对象)一定是可迭代对象,而迭代器对象即为平时所说的迭代器。
  • 可以使用for循环访问的均是可迭代对象,不一定要求是迭代器对象。至于for循环是访问什么元素,若类中有__iter__函数,则看该函数返回的是哪一个迭代器对象,根据该迭代器对象是怎么来的,来看具体是使用该迭代器对象中的__next__方法还是__getitem__() 方法返回for循环的每一个元素;若没有__iter__但是含有__getitem__(),则根据该函数按下标返回for循环的每一个元素。
  • 可以使用next()函数访问的只有迭代器对象。有两种情况,一是实现了__next__方法的迭代器类的实例对象;二是iter()函数作用于可迭代对象。
  • 集合数据类型如listdictstr等是可迭代类(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魔术方法】迭代器(iternext)
python的迭代器为什么一定要实现iter方法? - asialine的回答 - 知乎
Python 中的黑暗角落(一):理解 yield 关键字

------ 本文结束------
坚持原创技术分享,您的支持将鼓励我继续创作!

欢迎关注我的其它发布渠道