当我在自己的程序中发现用到了模式,我觉得这就表明某个地方出错了。程序的形式应该仅仅反映它所要解决的问题。代码中其他任何外加的形式都是一个信号,(至少对我来说)表明我对问题的抽象还不够深——这通常意味着自己正在手动完成事情,本应该通过写代码来让宏的扩展自动实现。
——Paul Graham, Lisp 黑客和风险投资人
Python 内置了迭代器模式,用于进行惰性运算,按需求一次获取一个数据项,避免不必要的提前计算。
迭代器在 Python 中并不是一个具体类型的对象,更多地使指一个具体协议。
- 所有的生成器都是迭代器。它们都实现了迭代器接口,区别于迭代器用于从集合中取出元素,生成器用来生成元素。
迭代器协议
Python 解释器在迭代一个对象时,会自动调用 iter(x)
。
内置的 iter
函数会做以下操作:
- 检查对象是否实现了
__iter__
方法(abc.Iterable
),若实现,且返回的结果是个迭代器(abc.Iterator
),则调用它,获取迭代器并返回; - 若没实现,但实现了
__getitem__
方法(abc.Sequence
),若实现则尝试从 0 开始按顺序获取元素并返回; - 以上尝试失败,抛出
TypeError
,表明对象不可迭代。
判断一个对象是否可迭代,最好的方法不是用 isinstance
来判断,而应该直接尝试调用 iter
函数。
注:可迭代对象和迭代器不一样。从鸭子类型的角度看,可迭代对象 Iterable
要实现 __iter__
,而迭代器 Iterator
要实现 __next__
. 不过,迭代器上也实现了 __iter__
,用于返回自身。
迭代器的具体实现
《设计模式:可复用面向对象软件的基础》一书讲解迭代器设计模式时,在“适用性”一 节中说:
迭代器模式可用来:
-
访问一个聚合对象的内容而无需暴露它的内部表示
-
支持对聚合对象的多种遍历
-
为遍历不同的聚合结构提供一个统一的接口(即支持多态迭代)
为了“支持多种遍历”,必须能从同一个可迭代的实例中获取多个独立的迭代器,而且各个迭代器要能维护自身的内部状态,因此这一模式正确的实现方式是,每次调用 iter(my_iterable)
都新建一个独立的迭代器。
序列可迭代的原因:iter
函数
解释器需要迭代对象 x 时,会自动调用 iter(x)
:
-
检查对象是否实现了
__iter__
方法并调用,获取到迭代器 -
如果没有实现
__iter__
, 检查是否有__getitem__
函数,尝试按顺序下标获取元素 -
如果上述状况都不符合, 抛出 “C object is not iterable” 异常
这就是为什么这个示例需要定义 SentenceIterator
类。所以,不应该把 Sentence 本身作为一个迭代器,否则每次调用 iter(sentence)
时返回的都是自身,就无法进行多次迭代了。
1 | # 通过实现迭代器协议,让一个对象变得可迭代 |
Return·a·list·of·all·non·overlapping·matches·in·the·string·
上面的例子中,我们的 SentenceIterator
对象继承自 abc.Iterator
通过了迭代器测试。而且 Iterator
替我们实现了 __iter__
方法。
但是,如果我们不继承它,我们就需要同时实现 __next__
抽象方法和实际迭代中并不会用到的 __iter__
非抽象方法,才能通过 Iterator
测试。
可迭代对象与迭代器的比较
-
可迭代对象
使用 iter 内置函数可以获取迭代器的对象。
-
迭代器
迭代器是一种对象:实现了
__next__
方法,返回序列中的下一个元素,并在无元素可迭代时抛出StopIteration
异常。
生成器函数
生成器函数的工作原理
-
只要函数的定义体中有
yield
关键字,该函数就是生成器函数。 -
调用生成器函数会返回生成器对象。
如果懒得自己写一个迭代器,可以直接用 Python 的生成器函数来在调用 __iter__
时生成一个迭代器。
注:在 Python 社区中,大家并没有对“生成器”和“迭代器”两个概念做太多区分,很多人是混着用的。不过无所谓啦。
1 | # 使用生成器函数来帮我们创建迭代器 |
1 | # 使用 re.finditer 来惰性生成值 |
案例:使用 itertools
模块生成等差数列
1 | # 实用模块 |
1 | # 使用 yield from 语句可以在生成器函数中直接迭代一个迭代器 |
[1, 2, 3, 4, 5] [1, 2, 3, 4, 5]
iter
函数还有一个鲜为人知的用法:传入两个参数,使用常规的函数或任何可调用的对象创建迭代器。这样使用时,第一个参数必须是可调用的对象,用于不断调用(没有参数),产出各个值;第二个值是哨符,这是个标记值,当可调用的对象返回这个值时,触发迭代器抛出 StopIteration 异常,而不产出哨符。
1 | # iter 的神奇用法 |