212. 迭代器

【导言】

大家一定都对for loop 很熟悉,也很会使用enumerate()等iteration function (迭代函数)不陌生,绝大多数程式语言都有自己的Iteration 功能与机制,今天会将Python 中Iteration / Iterable / Iterator 三个常混用的名词厘清,并且深入理解Iterable / Iterator 的运作机制,以及如何写出自己的Iterable / Iterator 运用到自己的程式码里。最后的最后,会针对常出现在Iteration 中yieldGeneratorobject 做补充。

【范例环境与建议先备知识】

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,rangestr都是Iterable .但不是Iterator ! 要检验一个object 是不是iterator 有以下方法测试:

  1. 使用collections中的Iterable和Iterator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from collections import Iterable, Iterator

x_0 = [1, 2, 3]
print(isinstance(x_0, Iterable), isinstance(x_0, Iterator))

# True False

x_1 = (1, 2, 3)
print(isinstance(x_1, Iterable), isinstance(x_1, Iterator))

# True False

x_2 = range(3)
print(isinstance(x_2, Iterable), isinstance(x_2, Iterator))

# True False

x_3 = "123"
print(isinstance(x_3, Iterable), isinstance(x_3, Iterator))

# True False

2.根据定义,利用dir()或是hasattr()去检查attributes __iter__,__getitem____next__

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
x = [1, 2, 3]

### Method 1: use "dir()"
print(dir(x))
# ['__add__', '__class__', '__contains__', '__delattr__', '__delitem__',
# '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__',
# '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__',
# '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__',
# '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__',
# '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__',
# '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index',
# 'insert', 'pop', 'remove', 'reverse', 'sort']

### Method 2: use "hasattr()"
print(hasattr(x, '__iter__'))
# True
print(hasattr(x, '__getitem__'))
# True
print(hasattr(x, '__next__'))
# Fals

由上面程式码,两种方法都显示,listobject 有__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__的几种常见写法。

  1. 利用__iter____next__实作一个Iterator:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MyIterator:
def __init__(self, max_num):
self.max_num = max_num
self.index = 0

def __iter__(self):
return self

def __next__(self):
self.index += 1
if self.index < self.max_num:
return self.index
else:
raise StopIteration

my_iterator = MyIterator(3)
for item in my_iterator:
print(item)
# 1
# 2

for item in my_iterator:
print(item)
# (no result)

由范例程式码可以看到实作了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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyIterator:
def __init__(self, max_num):
self.max_num = max_num

def __iter__(self):
num = 1
while num <= self.max_num:
yield num
num += 1

my_iterator = MyIterator(3)

for item in my_iterator:
print(item)
# 1
# 2
# 3
for item in my_iterator:
print(item)
# 1
# 2
# 3

上面的程式码逻辑上和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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyIterator:
def __init__(self, max_num):
self.max_num = max_num

def __getitem__(self, key):
if key <= self.max_num:
return key
else:
raise IndexError

my_iterator = MyIterator(3)

for item in my_iterator:
print(item)
# 0
# 1
# 2
# 3

这个方式直观许多,首先要注意__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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def generator_func(value=0):
while value < 10:
value = yield value
value += 1

generator = generator_func()

print('step 1')
print(next(generator))
print('step 2')
print(generator.send(1))
print('step 3')
print(generator.send(7))
print('step 4')
print(generator.send(10))

# step 1
# 0
# step 2
# 2
# step 3
# 8
# step 4
# Traceback (most recent call last):
# File "/Users/jackcheng/Programming/python/notes/notes_11.py", line 106, in <module>
# print(generator.send(10))
# StopIteration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
x = (i for i in range(2))

print(type(x))
# <class 'generator'>

print(next(x))
print(next(x))
print(next(x))

# 0
# 1
# Traceback (most recent call last):
# File "/Users/jackcheng/Programming/python/notes/notes_11.py", line 125, in <module>
# print(next(x))
# StopIteration

搭配上面的程式码,介绍一下yieldGeneratorobject:

  1. yield只能出现在function 里,而call 带有yield的function 会回传一个Generatorobject。
  2. Generatorobject 是一个Iterator,带有__iter____next__attributes。
  3. 第一次callnext(generator)执行内容等价于将原function 执行到第一次出现yield之处,「暂停」执行,并返回yield后的值。
  4. 第二次之后每次callnext(generator)都会不断从上次「暂停」处继续执行,直到function 全部执行完毕并raise StopIteration。因为yield没有将原function 从call stack 中移除,故能暂停并重新回到上次暂停处继续执行。这逻辑也是yieldreturn最核心不同之处,return会直接将原function 从call stack 中移除,终止function,不论return后面是是否还有其他程式码。
  5. yeild除了可传出值外,也可以接受由外部输入的值,利用generator.send()即可同时传入值也传出值。此设计,让Generatorobject 可和外部进行双向沟通,可以传出也可以传入值。
  6. 关于Generatorobject 的创建有两种语法:一是在function 内加入yield」,二是形如x = (i for i in y)的方式。其实大家常用的产生list 的其中一种写法x = [i for i in range(10)]就是创建一个Generatorobject 的变形。

概念性总结一下,原先和return搭配的function,就是吃进input 然后输出output,life cycle 就会消失,yield的写法可以视为扩充function 的特性,使其具有「记忆性」、「延续性」的特质,可以不断传入input 和输出output,且不杀死function。未来要撰写具有该特质的function 时就可以考虑使用yield来取代「在外部存一堆buffer 变数」的做法。

结语

了解Iteration 各种名词确切的定义并不是玩玩文字游戏,而是在开发时能较快速正确解读官方文件内容并更有效的撰写程式码,然后慢慢体会不同写法在不同情境需求下的优缺点。

此外Iteration 的概念也可以在许多Python 套件或是框架下适用,例如Pytorch 的Dataset就有提供Dataset(适用__getitem__)和IterableDataset(适用__iter__)两种框架,此时厘清两者差别后便可以根据需求挑选合适的框架。

【参考资料】