首页 > Python笔记 阅读数:9

Python迭代器详解

迭代是 Python 最强大的功能之一,是访问集合元素的一种方式。顾名思义,迭代器就是用于迭代操作(如 for 循环、while 循环)的对象,它可以像列表一样迭代获取其中的每一个元素。下面我们来介绍迭代器的使用方法。

Python可迭代对象

在 Python 中,有很多好用的数据类型,如列表、元组、字典、集合、字符串等。事实上,这些所谓的“数据类型”,更确切地说是存储数据的容器(container)。操作这些容器时,我们常需要逐个访问其中的元素。这种逐个获取容器中元素的过程,就叫“迭代”(iteration)。

简单来说,具备可迭代访问特性的对象,就叫作可迭代对象。这样说起来有点抽象,我们先用一个形象的案例来说明,如图 1 所示。

可迭代对象示意图
图 1:可迭代对象示意图

在图 1 左侧所示的代码中可以看出,在输入 In [2] 和 In [3] 处,分别用全局函数 iter( ) 定义了两个独立的迭代器 y 和 z,它们都“指向”列表 x。然后,通过 next( ) 函数可逐个访问可迭代对象的下一个元素。

所谓迭代器,简单来说,我们可以把它理解为能够访问容器元素的“智能指针”。在 C、C++ 中,我们常用指针(即对象在内存中的地址)指向一个数组,然后通过“+1”操作来访问“下一个”元素。

这里之所以说迭代器是“智能指针”,是因为,在 Python 中迭代器是作为一个迭代类的对象而存在的。既然是对象,它就有一些成员函数(或方法)可供使用,在函数(或方法)内,我们可以添加更多具体的操作,从而表现出比纯粹的指针更多的“智能”。

比如说,在 C、C++ 中,通过指针访问数组元素时,编译器是不做边界检查的,一旦越界,程序就可能崩溃。但迭代器可以通过 StopIteration 异常来标识迭代的完成,并进行合理的异常处理,这样程序就能正常运行,如例 1 所示。

【例 1】迭代器的边界检查(iterater.py)
my_list = [1,2,3,4]
#创建迭代器对象
it = iter(my_list )

while True:
    try:
        print (next(it))
    except StopIteration: 
        print ("迭代器越界啦!")
        break
print ("我能正常输出!")
程序执行结果为:

1
2
3
4
迭代器越界啦!
我能正常输出!


迭代器内部维护着一个状态,该状态用来记录当前迭代“指针”所在的位置,以方便下次迭代时(如第 07 行所示的 next( ) 函数)获取正确的元素。一旦所有元素都被遍历,那么迭代器就会指向容器对象的尾部,并触发停止迭代的异常(第 08 行)。

迭代器有一个显著的特点,那就是惰性估值(Lazy evaluation)。其含义在于,只有当迭代至某个值时,该元素才会被计算并获取。这个特性有点像“抽一鞭子走一步”的懒牛。

存在即合理。这种“懒”也是有优点的,即迭代器特别适合用于遍历大文件或无限集合,因为我们不用一次性将它们全部预存到内存之中,用哪个再临时拿来即可。

Python创建迭代器

在 Python 中,一切皆对象。迭代器也不例外,具体的迭代器实际上是某个迭代类定义的对象。比如 list_iterator 是列表类迭代器的对象,set_iterator 是集合类迭代器的对象,以此类推。

所有的迭代器在设计之时通常都会在类中实现两个方法:__iter__() 和 __next__()。__iter__() 方法用于返回一个迭代器对象,__next__() 方法用于返回迭代对象内部的下一个元素值。

为了更直观地感受迭代器内部的执行过程,我们改造《Python生成器是什么(超级详细)》一节中的例 1,创建一个迭代器,依然以斐波那契数列为例来进行说明。

【例 1】创建迭代器(iterate-fib.py)
from itertools import islice
class Fibonacci:
    def __init__(self):
        self.previous, self.current = 0,1

    def __iter__(self):
        return self

    def __next__(self):
        value = self.current
        self.previous, self.current = self.current, self.current + self.previous
        return value

f = Fibonacci()
a = list(islice(f, 0, 10))
print (a)
#b = list(islice(f, 0, 10))
#print(b)
程序执行结果为:

[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]


Python 中有一个内置的模块 itertools,该模块中包含了一系列用来产生不同类型迭代器的函数或类,它们都可以产生一个迭代器,然后通过 for/while 循环来遍历取值,当然也可以使用全局内置函数 next( ) 来取值。

第01行代码就是导入这个模块的一个常用函数,其函数原型如下。
islice(iterable, start,stop[, step])

该函数的第一个参数就是一个可迭代对象,随后的参数分别是迭代对象的起始位、终止位和步长。它的用法和列表及元组的“切片”函数非常类似。事实上,islice就是“迭代分片”的意思,其中“i”表示“iterable”(可迭代对象),“slice”表示“分片”。但是,这个迭代切片不支持负数索引。

第 02~12 行定义了一个 Fibonacci 类。随后,该类定义了一个对象 f,它是一个可迭代对象(第 14 行),这是因为 Fibonacci 类实现了 __iter__() 方法。与此同时,f又是一个迭代器,因为它实现了 __next__() 方法。该方法保障了变量 self.previous 和 self.current 在迭代器内部的状态更新。

每次调用 next( ) 方法的时候,__next__() 会在背后默默做如下两件事。
简单来说,迭代器就像一个惰性加载的工厂,等到有人需要时,它才加工产品,返回生成值,当没人搭理的时候,它就处于休眠状态,等待下一次调用来唤醒。

如果我们把第 17~18 行的注释去掉,运行的结果会在上述运行结果的基础上增加如下内容。

[89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]


第 18 行和第 20 行的代码完全一样,但输出结果迥然不同,你知道为什么吗?请自行思考其中的原因。

相关文章