目录
一、绪论
事实上,Python、C++、JAVA 等语言中都有 迭代器 的概念,用法相近,均旨在解决 “如何在复杂场景下尽可能简便地获取数据” 的问题。迭代器的含义类似于遍历链表时所用到的 cur 指针 —— 总是指向当前位置并知道下一个位置 next。
封面图片 较好地展现了 Python 可迭代对象 (iterable)、迭代器 (iterator)、生成器 (generator) 等概念的基本联系。为突出其重要性以及便于查阅,特置于醒目的位置。(来自网络)
二、可迭代对象 (iterable)
可迭代对象 (iterable) 是一种 能逐一返回其成员项 的对象,常包括:
- 所有内置序列对象 (字符串、列表、元组)
- 部分内置非序列对象 (字典、集合、文件对象等)
- 其他支持迭代协议的对象 (定义有 __iter__() 方法)
- 其他支持序列协议的对象 (定义有 __getitem__() 方法)
既可用内置函数 hasattr() 检查一个对象是否支持迭代协议 (定义有 __iter__() 方法),以判断其是否为可迭代对象:
# 可见以下常见的内置类型均支持迭代协议 —— 为可迭代对象
>>> hasattr([], "__iter__"), hasattr({}, "__iter__"), hasattr('', "__iter__"), hasattr(set(), "__iter__"), hasattr((), "__iter__")
(True, True, True, True, True)
也可用内置函数 isinstance() 并结合 collections 模块中的抽象基类 (ABC),以判断一个对象是否为可迭代对象:
>>> from collections import Iterable
# 测试常见数据类型的对象, 可见均为可迭代对象(iterable)
>>> isinstance([], Iterable), isinstance({}, Iterable), isinstance('', Iterable), isinstance(set(), Iterable), isinstance((), Iterable)
(True, True, True, True, True)
可迭代对象可用于 for 循环 及各种 以 iterable 为形参的函数/方法 中 (如 zip()、map()、enumerate() 等),例如:
>>> help(map)
Help on class map in module builtins:
class map(object)
| map(func, *iterables) --> map object
|
| Make an iterator that computes the function using arguments from
| each of the iterables. Stops when the shortest iterable is exhausted.
|
| Methods defined here:
|
| __getattribute__(self, name, /)
| Return getattr(self, name).
|
| __iter__(self, /)
| Implement iter(self).
|
| __new__(*args, **kwargs) from builtins.type
| Create and return a new object. See help(type) for accurate signature.
|
| __next__(self, /)
| Implement next(self).
|
| __reduce__(...)
| Return state information for pickling.
由此可知,map 派生自 object,其形参包含了可迭代对象 iterables。不仅如此,map 内部定义了 __iter__() 方法 (支持迭代协议),因此 map 对象也是可迭代对象。而 zip()、enumerate() 等函数亦复如是。
可迭代对象 作为 实参 传递给 内置函数 iter() 时,将 返回一个该可迭代对象的迭代器 (irerator)。这种迭代器适用于对 值/数据成员/元素 的集合的 一次性遍历。使用可迭代对象时,通常无需自行调用 iter() 或自行处理迭代器对象,for 语句会在内部处理好这些操作 —— 创建一个临时量 (临时的未命名变量) 用于在循环/迭代/遍历期间保存迭代器。
关于 for 语句,简要回顾如下:
- for 语句用于对序列 (str、list、tuple) 等 可迭代对象中的元素执行顺序迭代。
- for 语句在迭代前,先 创建一个该可迭代对象的迭代器 (irerator),并用一个临时量 (临时的未命名变量) 保存该迭代器。迭代过程 即 顺序遍历和操作迭代器的元素。当 迭代器元素耗尽 时 (包括序列为空 或 迭代器引发 StopIteration 异常等),执行 else 语句 (如果有),然后结束 for 循环 (每次 for 循环都会有一个 内部计数器 用于跟踪下一个元素,每次迭代都会使计数器递增,当计数器值增至迭代器元素总数时循环就会终止)。
- 若 for 语句中执行了 break,则直接跳过 (for 循环的) else 语句并结束 for 循环。
- 若 for 语句中执行了 continue,则跳过本次迭代,直接进行下一次。
- for 语句的内部机制 确保不因迭代器元素耗尽引发 StopIteration 异常 (例如使用 try-except 语句 详见下节)。
例如,经常使用 for 语句对 range() 函数对象迭代,而 range() 函数对象就是一个可迭代对象 (但不是迭代器):
>>> for i in range(1):
pass
>>> isinstance(range(1), Iterable), isinstance(range(1), Iterator)
(True, False)
注意,上述提到了将要进一步说明的概念 —— 迭代器 (irerator)。
三、迭代器 (iterator)
3.1 介绍
迭代器 (iterator) 是一种用于表示 一连串数据流 的对象。
迭代器对象要求支持 迭代器协议 —— 对象须同时支持/实现 __iter__() 方法和 __next__() 方法。协议规定:
- iterator.__iter__() 方法 返回迭代器对象本身 (对可迭代对象使用内置函数 iter() 将返回迭代器),这是同时允许容器和迭代器配合 for .. in ... 语句使用所必须的。
- iterator.__next__() 方法 返回容器的下一项元素。重复调用迭代器的 __next__() 方法 (或用内置函数 next()) 将依次返回数据流中的元素,并在元素耗尽 (无数据项/元素) 时引发 StopIteration 异常。迭代器对象中的数据项/元素耗尽后,再次调用 __next__() 或 next() 只会再次引发 StopIteration 异常。
迭代器必须具有 __iter__() 方法用来返回该迭代器对象自身,因此 迭代器必为可迭代对象 (但可迭代对象不一定是迭代器)。换言之,只有迭代器有 __next__() 方法,而可迭代对象没有。
迭代器可用于其他可迭代对象适用的大部分场合 (比如 map 同时实现了 __iter__() 和 __next__() 方法,故其亦为迭代器)。
>>> from collections import Iterable, Iterator # 导入两个抽象基类, 可用于检查对象类型
>>> lst = [0, 1, 2] # 创建示例列表 lst
>>> isinstance(lst, Iterable), isinstance(lst, Iterator) # lst 是可迭代对象, 但还不是迭代器
(True, False)
>>> iterator = iter(lst) # 返回可迭代对象 lst 的迭代器, 并由变量 iterator 指向/保存该迭代器
>>> isinstance(iterator, Iterable), isinstance(iterator, Iterator) # iterator 既是可迭代对象, 也是迭代器
(True, True)
>>> type(iterator) # 更具体地, 这是一个 “列表迭代器 (list_iterator)”
<class 'list_iterator'>
>>> next(iterator) # 使用内置函数 next(), 输出一个元素
0
>>> iterator.__next__() # 等价于使用成员方法 __next__(), 输出一个元素
1
>>> next(iterator) # next() 和 __next__() 效果完全相同
2
>>> next(iterator) # 迭代器元素耗尽, 调用 next() 引发 StopIteration 异常
Traceback (most recent call last):
File "<pyshell#32>", line 1, in <module>
next(iterator)
StopIteration
>>> next(iterator) # 迭代器元素耗尽, 再次调用 next() 仍会引发 StopIteration 异常
Traceback (most recent call last):
File "<pyshell#33>", line 1, in <module>
next(iterator)
StopIteration
综上可知,迭代器表示一个 数据流,可被 next() 函数不断调用,从首项开始不断返回下一项数据/元素 (而不能回退) 直至耗尽,并抛出 StopIteration。
事实上,不断调用 next() 函数,内部指针随之后移并依次访问各元素;当遍历尽所有元素时,意味着指针移到了最后一个元素后,故不论怎么调用 next() 函数,都无法访问到元素,于是引发 StopIteration 异常;若想要从头访问,须再次载入迭代器对象。
而该 数据流 可视为一个 未知长度的 “有序序列” (比喻),只能通过不断调用 next() 按需计算下一项数据/元素。因此,迭代器是 惰性的,需要返回下一个项时才会计算 (形同赶牛,抽一下动一下)。
事实上,若有必要,迭代器甚至可表示为一个 无限大的数据流,例如全体自然数。而有限长度的序列/非序列等是无法做到存储全体自然数的。
由此可知,for 循环的本质即:对可迭代对象调用 iter() 返回一个关于该对象的迭代器,然后不断调用 next() 迭代/遍历元素。例如:
>>> for i in [0, 1, 2]:
print(i)
0
1
2
# --------------------------------------- 等价于 ----------------------------------------
>>> iterator = iter([0, 1, 2]) # 将从可迭代对象(list)返回一个迭代器
>>> while True:
try: # 使用 try-except 语句捕获异常, 避免抛出 StopIteration 异常并正常结束循环
j = next(iterator) # 返回迭代器的下一个元素
print(j)
except StopIteration: # 当元素耗尽时, 捕获 StopIteration 异常并退出循环/迭代
break
0
1
2
这就是为什么我们 从未见过 for 语句引发 StopIteration 异常 —— 它可以使用 try-except 语句 或其他相关代码实现 异常捕获、处理与自动结束。
此外,不建议对 可变对象 边迭代边修改,因为如此将可能带来各种 麻烦和困扰。
3.2 文件迭代器
设 test.txt 文件的内容为:
First Line
Last Line
通常使用 read() / readline() / readlines() 等方式读取文件内容,例如:
>>> fin = open('test.txt')
>>> fin.readline()
'First Line\n'
>>> fin.readline()
'Last Line'
>>> fin.readline() # fin.readline() 若返回一个空字符串, 说明已经已经读取到最后一行
''
>>> fin.close()
当然,用 for 循环更便捷:
>>> fin = open('test.txt')
>>> for line in fin:
print(line, end='') # print() 函数默认结尾是换行符 \n , 这里手动改成 ''
First Line
Last Line
>>> fin.close()
其实,还可以使用 文件迭代器,例如:
>>> fin = open('test.txt') # 文件对象
>>> fin.__next__()
'First Line\n'
>>> fin.__next__()
'Last Line'
>>> fin.__next__()
Traceback (most recent call last):
File "<pyshell#8>", line 1, in <module>
fin.__next__()
StopIteration
>>> fin.close()
可见,还可使用 __next__() 方法 (当然 next() 亦可) 逐行读取文件内容,这说明 文件对象是迭代器:
>>> from collections import Iterable, Iterator # 抽象基类
>>> fin = open('test.txt')
>>> isinstance(fin, Iterable), isinstance(fin, Iterator) # 文件对象 fin 也是迭代器
(True, True)
正如 3.1 节结尾所述:for 循环本质就是对可迭代对象调用 iter() 返回一个迭代器、然后不断调用 next() 迭代/遍历元素。
3.3 自定义类实现迭代器
除了 Python 内置迭代器对象,更主要的用法还是 自定义迭代器 —— 自定义类并在其成员方法中同时实现 __iter__() 方法和 __next__() 方法 (以支持迭代器协议) 创建迭代器。例如:
>>> class Counter:
''' 自定义一个示例迭代器类 (简化到甚至构造方法 __init__() 都不实现) '''
def __iter__(self): # __iter__() 方法用于初始化并返回迭代器
self.num = 0
return self # 只需返回自身
def __next__(self):
i = self.num
self.num += 1
return i
>>> counter = Counter() # 实例化一个类对象
>>> isinstance(counter, Iterable), isinstance(counter, Iterator) # 支持迭代器协议, 是迭代器
(True, True)
>>> count_iter = iter(counter) # 返回一个迭代器
>>> isinstance(count_iter, Iterable), isinstance(count_iter, Iterator) # 迭代器
(True, True)
>>> next(count_iter) # 理论上可以无限迭代所有的自然数
0
>>> next(count_iter)
1
>>> next(count_iter)
2
StopIteration 异常 用于 标识迭代的完成,防止出现无限循环 的情况,在 __next__() 方法中可以自行设置在完成指定循环次数后触发 StopIteration 异常以结束迭代:
>>> class Counter2:
def __iter__(self):
self.num = 0
return self
def __next__(self):
if self.num <= 3: # 自定义上迭代上限/停止迭代条件
i = self.num
self.num += 1
return i
else:
raise StopIteration # 主动抛出异常
>>> counter2 = Counter2()
>>> count_iter2 = iter(counter2)
>>> next(count_iter2)
0
>>> next(count_iter2)
1
>>> next(count_iter2)
2
>>> next(count_iter2)
3
>>> next(count_iter2)
Traceback (most recent call last):
File "<pyshell#84>", line 1, in <module>
next(count_iter2)
File "<pyshell#77>", line 11, in __next__
raise StopIteration
StopIteration
当然,最好还是 初始化构造函数 使之 更灵活:
>>> class Counter3:
def __init__(self, start, end):
self.start = start
self.end = end
def __iter__(self):
self.indicator = self.start
return self
def __next__(self):
if self.indicator <= self.end:
i = self.indicator
self.indicator += 1
return i
else:
raise StopIteration
>>> counter3 = Counter3(0, 2) # 实例化类对象时, 自定义传入计数初始值和结束值
>>> count_iter3 = iter(counter3)
>>> next(count_iter3)
0
>>> next(count_iter3)
1
>>> next(count_iter3)
2
>>> next(count_iter3)
Traceback (most recent call last):
File "<pyshell#93>", line 1, in <module>
next(count_iter3)
File "<pyshell#86>", line 14, in __next__
raise StopIteration
StopIteration
此外,迭代器同样具有 缺点,例如:
- 迭代器只能不断往下一项元素迭代,而不能回退
- 迭代器不适用于多线程环境下的可变集合等
四、相关内置函数
4.1 返回迭代器对象 —— iter()
内置函数 iter() 通过可迭代对象 返回一个关于可迭代对象的迭代器对象,其函数原型为:
iter(object[, sentinel])
其中,第一个参数 object 必须是 支持迭代协议 (有 __iter__() 方法) 或 支持序列协议 (有 __getitem__() 方法,且数字参数从 0 开始) 的集合对象。若不支持这些协议,会引发 TypeError。只传入第一个参数 object 并返回一个迭代器的用法十分常见,上文例子很多不再赘述。
注意,若还传入第二个参数 sentinel,则参数 object 必须是一个 可调用 (callable) 的对象 (如函数)。这种情况下生成的迭代器对象,每次调用其 __next__() 方法时,都会不带实参地调用 object。若返回的结果是 sentinel 则触发 StopIteration,否则返回调用结果。例如:
>>> callable(list) # 使用内置 callable() 函数判断是否为可调用对象 (callable object)
True
# ------------------------------------------------------------------------------
>>> reader1 = iter(list, '') # 参数 sentinel='', 迭代器只有返回 '' 时才会停止
>>> next(reader)
[]
>>> next(reader)
[]
# ------------------------------------------------------------------------------
>>> reader2 = iter(list, []) # 参数 sentinel='', 迭代器只有返回 [] 时才会停止
>>> next(reader2)
Traceback (most recent call last):
File "<pyshell#128>", line 1, in <module>
next(reader2)
StopIteration
可见,使用内置 callable() 函数可判断对象是否可调用。而若类实现了 __callable__() 方法,则其实例是可调用的。
当然,上例仅是说明语法,而非正常用法。有一个经典用法是:读取文件的行直至到达某一特定行为止。下例读取一个文件,直到 readline() 方法返回一个空字符串:
>>> with open('test.txt') as file: # with 语句下的上下文管理器
for line in iter(file.readline, ''): # 逐行读取并迭代
process_line(line) # 处理该行的统称/笼统表示方式
4.2 调用下一个元素 —— next()
内置函数 next() 通过 调用迭代器的 __next__() 方法返回下一项元素,其函数原型为:
next(iterator[, default])
其中,第一个参数 iterator 为 迭代器对象。第二个参数 default 可选,用于设置 在没有下一项元素时返回的默认值/缺省值,以避免抛出异常中断程序。若不设置参数 default,且调用 next() 时无下一项元素则会触发 StopIteration 异常。例如:
>>> list_iter1 = iter([0, 1, 2])
>>> next(list_iter1)
0
>>> next(list_iter1)
1
>>> next(list_iter1)
2
>>> next(list_iter1) # 不指定 default 且迭代器元素耗尽, 将引发 StopIteration 异常
Traceback (most recent call last):
File "<pyshell#117>", line 1, in <module>
next(list_iter1)
StopIteration
>>> next(list_iter1, 'stop') # 指定 default, 则迭代器元素耗尽, 将输出 default
'stop'
上例不仅说明了 迭代器元素耗尽时抛出 StopIteration 异常的原因,还给出了 next() 函数的另一种用法来避免引发异常。
事实上,除上述方式外,还存在另一种形式的迭代器 —— 生成器(generator),详见下节:《【Python】详解 生成器(generator) —— 迭代器完全解读(下) 》。
参考文献: