212. 迭代器
【导言】
大家一定都对for loop 很熟悉,也很会使用enumerate()
等iteration function (迭代函数)不陌生,绝大多数程式语言都有自己的Iteration 功能与机制,今天会将Python 中Iteration / Iterable / Iterator 三个常混用的名词厘清,并且深入理解Iterable / Iterator 的运作机制,以及如何写出自己的Iterable / Iterator 运用到自己的程式码里。最后的最后,会针对常出现在Iteration 中yield
和Generator
object 做补充。
【范例环境与建议先备知识】
OS: Ubuntu: 16.04
Python: 3.7
Required: Knowledge
- 了解Python Class 以及function/method 基础概念
- 了解Python magic function
定义Python的Iteration/Iterable/Iterator
每个程式语言(例如C++、Java等)都有自己一套运行Iteration 的设计与机制,下方先定义Python 的三个常用名词Iteration / Iterable / Iterator 以利同步讨论以及书写内容。
- Iteration:走访/迭代/遍历一个object 里面被要求的所有元素之「过程」或「机制」。是一个概念性的词。
- Iterable:可执行Iteration 的objects 都称为Iterable(可当专有名词)。参照官方文件提及,是指可以被for loop 遍历的objects。以程式码来说,只要具有__iter__或__getitem__的objects 就是Iterable。
- 只要具有__iter__和__next__的objects 皆为Iterator。 Iterator 是Iterable 的subset。
最容易被误认的是Python 常见的list
, tuple
,range
和str
都是Iterable .但不是Iterator ! 要检验一个object 是不是iterator 有以下方法测试:
- 使用collections中的Iterable和Iterator
1 | from collections import Iterable, Iterator |
2.根据定义,利用dir()
或是hasattr()
去检查attributes __iter__
,__getitem__
和__next__
1 | x = [1, 2, 3] |
由上面程式码,两种方法都显示,list
object 有__iter__
以及__getitem__
但没有__next__
,所以是Iterable 但不是Iterator。但是,透过call iter(x)
(其实就是call x.__iter__()
) ,会回传一个同时带有__iter__
和__next__
的新的object x_iter
,此时x_iter
就是一个Iterator 了!
了解__iter__, __getitem__, __next__
要了解这三个attributes 最直接的例子就是透过for loop 的运作机制来解说。
制图师(自己),来,请下图!

当你轻松call for loop 时,其实发生了上图所绘的步骤。
值得注意的是,可以搭配for loop 的object,不需要一定是Iterator,Iterable 就可以了。
接下来要示范如何在自己的class 中实作__iter__
,__next__
和__getitem__
的几种常见写法。
- 利用
__iter__
和__next__
实作一个Iterator:
1 | class MyIterator: |
由范例程式码可以看到实作了MyIterator
的__iter__
和__next__
,且__iter__
仅仅是回传self
,原因是该object 已经有__next__
故本身就是一个Iterator ,所以直接回传即可。可以看到我透过instance attributeself.index
来作为要iterate 出去的值,而当触发终止iteration 条件时, raiseStopIteration
即可终止。
但可以很轻易的知道,这样的设计很怪,因为self.index
是instance scope variable ,产生的my_iterator
只能用for loop 一次。
为了解决这样的问题,此时可以引进yield
来实作,取代原本的design pattern。 (下一个例子中会示范)
2.利用__iter__
和Generator
(透过yield
产生) 来实作一个Iterable
1 | class MyIterator: |
上面的程式码逻辑上和python_iteration_2.py程式码相同。唯一比较奇怪的是,我拔掉了__next__
且__iter__
也没有出现return
等语法回传任何东西的样子,这样不就违反前面对于iterator 的定义了吗? !其实,当一个function (此处为__iter__
)中带有yield
时,该function 就会自动return 一个Gerenator
的object,而这generator 自带__next__
,是一个Iterator。如此,这样一切都又符合规范与定义了!注意,这里是实作Itareble 不是Iterator,故自身不一定要具有__next__
!
想要在iteration 过程中保留某些值供计算输出(此处为num
),就可以存于透过yield
产生generator 并存于该scope。此外,因为每次for loop 都会call__iter__
并产生新的generator,所以每次的num
都可以刷新重新来过,解决的原先的问题了!
若想要更进一步了解yield
的机制,可以参考本文中下一章节深入了解yield的介绍。
3.利用__getitem__
来实作一个Iterable
1 | class MyIterator: |
这个方式直观许多,首先要注意__getitem__(self, key)
一定要有第二个positional argument key
,for loop 会自动生成从0 开始到无穷的整数传入作为key
,故我们只需要根据key
来输出想要的值。当触发欲终止条件时,raiseIndexError
或是StopIteration
皆可顺利终止iteration。
此外,补充一点__getitem__
是常见的magic function (即Python 预设自订带有double underscores 的function),所以并不是只有在处理iteration 时会使用到,如index 和slice 等功能都会需要使用到此function,若在创建较为复杂的object 时,要记得根据__getitem__
的arguments 兼容处理各种况状。
最后,可能有人会问说为何Iteration 要设计__next__
和__getitem__
这两种pattern 呢?实作面来说,用__next__
这种方式写会更适合在处理、生成前后值相依或是与streaming 概念相似的资料,例如「生成Fibonacci Sequence」;而__getitem__
一般使用情境是已经将所有资料储存在某处、某变数中,然后透过key 来取值。
深入了解yield
在本文中的【贰、了解__iter__ / getitem / __next__】已提及yield
在撰写Iteration 时的使用方式,这里就一并补上关于yield
完整介绍啰!
先观察下面两段范例程式码:
1 | def generator_func(value=0): |
1 | x = (i for i in range(2)) |
搭配上面的程式码,介绍一下yield
与Generator
object:
yield
只能出现在function 里,而call 带有yield
的function 会回传一个Generator
object。Generator
object 是一个Iterator,带有__iter__
和__next__
attributes。- 第一次call
next(generator)
执行内容等价于将原function 执行到第一次出现yield
之处,「暂停」执行,并返回yield
后的值。 - 第二次之后每次call
next(generator)
都会不断从上次「暂停」处继续执行,直到function 全部执行完毕并raiseStopIteration
。因为yield
没有将原function 从call stack 中移除,故能暂停并重新回到上次暂停处继续执行。这逻辑也是yield
和return
最核心不同之处,return
会直接将原function 从call stack 中移除,终止function,不论return
后面是是否还有其他程式码。 yeild
除了可传出值外,也可以接受由外部输入的值,利用generator.send()
即可同时传入值也传出值。此设计,让Generator
object 可和外部进行双向沟通,可以传出也可以传入值。- 关于
Generator
object 的创建有两种语法:一是在function 内加入yield
」,二是形如x = (i for i in y)
的方式。其实大家常用的产生list 的其中一种写法x = [i for i in range(10)]
就是创建一个Generator
object 的变形。
概念性总结一下,原先和return
搭配的function,就是吃进input 然后输出output,life cycle 就会消失,yield
的写法可以视为扩充function 的特性,使其具有「记忆性」、「延续性」的特质,可以不断传入input 和输出output,且不杀死function。未来要撰写具有该特质的function 时就可以考虑使用yield
来取代「在外部存一堆buffer 变数」的做法。
结语
了解Iteration 各种名词确切的定义并不是玩玩文字游戏,而是在开发时能较快速正确解读官方文件内容并更有效的撰写程式码,然后慢慢体会不同写法在不同情境需求下的优缺点。
此外Iteration 的概念也可以在许多Python 套件或是框架下适用,例如Pytorch 的Dataset
就有提供Dataset
(适用__getitem__
)和IterableDataset
(适用__iter__
)两种框架,此时厘清两者差别后便可以根据需求挑选合适的框架。
【参考资料】
- Python 官方文件
- 极客学院-迭代器(Iterator)
- python yield 语法与generator 物件介绍