Python 中的迭代器和生成器
Python 中对象的遍历称为迭代,常用 for 循环和 while 循环等实现。那么有哪些对象可以迭代,它们有什么共同特性呢?本篇对可迭代对象、迭代器和生成器进行介绍。
迭代
对于 python 中的 list, tuple, dict, set, str,文件对象等容器(数据的集合,这里不使用集合为了区分 python 中的 set),我们可以使用 for 循环等遍历里面的元素,但没有按照固定的顺序进行:
1 | for i in set("a1$."): |
对于 C 语言,迭代都是按照索引顺序进行:
1 | for (i = 0; i < length; i++) { |
可见,python 的 for 循环的抽象程度是高于 C 的。因此,在 python 中对于没有索引的容器都可以进行迭代。如
1 | d = {"a": 1, "b": 2} |
想要迭代时增加索引序号:
1 | # 默认从 0 开始索引 |
那到底哪些对象可以进行 for 循环或者说可迭代呢?在 python 中,可迭代对象可以进行迭代,所谓可迭代对象就是类中包含有方法 __iter__()
的实例,一般该方法都是通过一些已知的可迭代对象来实现。此外,有些类中包含有方法__getitem__()
的也可以进行迭代。
1 | # 包含有 __iter__() 的类 |
1 | # 包含有 __getitem__() 的类 |
可迭代对象
python 中使用最多的且能够进行 for 循环的对象是可迭代(Iterable)对象,就是类中实现有方法 __iter__()
的,那么如何识别哪些对象是可迭代对象呢?可以采用如下方法:
1 | # 判断类中是否有 __iter__ 函数 |
这里再次总结一下 python 中哪些对象是可迭代对象:
- 容器对象,如 list, tuple, set, dict, str;
- 文件对象;
- 类中定义了
__iter__()
的实例对象;
对于文件对象,判断是否是可迭代对象:
1 | from pathlib import Path |
迭代器
从某种程度上,迭代器(Iterator)是可迭代对象的子集。迭代器就是同时实现了方法__iter__()
和 __next__()
的类。可以使用如下方法判断一个对象是否是迭代器:
1 | # 判断类中是否有 __iter__ 函数和 __next__ 函数 |
可迭代对象可以使用函数 iter()
转化为迭代器。迭代器对象还可以使用 next()
函数进行访问元素,没有数据是抛出 StopIteration 错误。
1 | a = [0, 1, 2] |
Python的迭代器对象表示的是一个数据流。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()
函数实现按需计算下一个数据,所以迭代器的计算是惰性的,只有在需要返回下一个数据时它才会计算。迭代器对象可以表示一个无限大的数据流,例如全体自然数。迭代器持有一个内部状态的字段,用于记录下次迭代返回值。迭代器不会一次性把所有元素加载到内存,而是在需要的时候才生成返回结果。
1 | # 无限序列 |
其实,对可迭代对象使用 for 循环,内部就是先将可迭代对象转化为迭代器,然后进行返回:
1 | # Python 的 for 循环本质上就是通过不断调用 next() 函数实现的 |
这里总结一下 python 中的迭代器对象:
- 容器对象使用
iter()
封装后,如 list, tuple, set, dict, str 的实例对象经过iter()
封装; - 文件对象;
- 类中定义了
__iter__()
和__next__()
的实例对象; - 生成器对象。
自定义迭代器类,以打印斐波那契数列为例:
1 | from itertools import islice, count, cycle |
生成器
生成器(generator)是一种特殊的迭代器,它可以直接通过 yield
关键字把函数转化为生成器。如下定义的函数就是一个生成器,同样可以使用 next()
函数范围元素:
1 | def FibonacciGenerator(): |
可以使用如下方法判断一个对象是不是生成器:
1 | from collections.abc import Generator |
这里总结一下 python 中的生成器对象:
- 使用
yield
定义生成器函数; - 列表生成器或生成器表达式 (generator expression)。
列表生成器是如下的形式:
1 | g = (x ** 2 for x in range(3)) |
注意:这里不要写成 g = [x ** 2 for x in range(3)]
,此时 g 是列表,它只是可迭代对象,不是迭代器,更不是生成器
当程序遇到yield
关键字时,这个生成器函数就返回了,直到再次执行了next()
函数,它就会从上次函数返回的执行点继续执行,即yield
退出时保存了函数执行的位置、变量等信息,再次执行时,就从这个yield
退出的地方继续往下执行。