3.2 迭代器与生成器

3.2.1 迭代器

1. 迭代器的规则

Python中的容器类型通常包含一个迭代器(Iterator)帮助它们支持for循环的操作。这些容器类型需要实现一个.__iter__()方法返回相应的迭代器:

<container>.__iter__()

常见的容器类型,如列表、集合、字典、元组等,都有一个对应的迭代器:

迭代器对象支持.next()方法,该方法返回容器中被迭代到的下一个元素。例如,对于列表的迭代器。

In [5]: x = [2, 4, 6]

In [6]: i = x.__iter__()

第一次调用.next()方法,返回第一个元素2:

In [7]: i.next()

Out[7]: 2

再次调用.next()方法时,返回可迭代对象的下一个元素:

迭代器是一种“一次性消费品”,迭代完最后一个元素后,调用.next()方法不会回到开头,而是抛出一个StopIteration异常:

for循环正好可以利用迭代器的这种性质。

当我们对一个容器类型进行循环时,Python首先使用它的.__iter__()方法得到它的迭代器,然后不断调用迭代器的.next()方法,在抛出StopIteration异常后停止循环。

迭代器对象本身也有一个.__iter__()方法,这个方法必须返回迭代器本身:

In [11]: i.__iter__() is i

Out[11]: True

有一些函数返回的结果是迭代器对象,例如:

In [12]: reversed(x)

Out[12]: <listreverseiterator at 0x49327f0>

2. 自定义迭代器

对于一个迭代器来说,它需要实现两个方法:

● .__iter__()方法,返回迭代器自身;

● .next()方法,对内容进行迭代,当内容被迭代完时,抛出一个StopIteration异常。实现了这两个方法的自定义类型都可以称为一个迭代器。

我们仿照函数reversed()的功能,来定义一个将列表反序的自定义迭代器。

自定义类型使用关键字class定义。按照迭代器的定义要求,需要实现的基本结构如下:

其中,self表示自定义对象本身,.__init__()是自定义类型中一个特殊的方法,用来初始化定义的类型。

我们的迭代器需要接受一个列表进行初始化,因此.__init__()方法接受一个列表x作为参数,并让对象的.seq属性存储列表x,.idx属性存储列表x的长度:

对于.next()方法,我们利用.idx属性来判断当前迭代到哪个元素:

● 初始情况下,.idx属性等于列表的长度,表示列表中剩下的元素;

● 每次调用.next()方法时,idx属性减1;

● 根据.idx属性的大小返回相应位置的元素;

● 当idx属性<0时,说明列表已被迭代完毕,抛出一个StopIteration异常。

完整的定义如下:

可以用列表初始化这个迭代器,并将这个迭代器与for循环一起使用:

这里用到了print的一个技巧:print默认会在输入的内容后自动加上回车,可以在输出内容后加上一个逗号“,”,让它不输出回车。

构造迭代器不一定需要容器对象。

例如,对一个正整数n,有如下迭代规则:

● 如果n是奇数,令n=3n+1;

● 如果n是偶数,令n=n/2;

Collatz猜想为:从任意的正整数n开始使用上述规则迭代,总能在有限次操作内使n为1。

我们利用此规则定义一个迭代器Collatz,该迭代器初始化接受一个正整数n,保存在.value属性中,作为序列的开始:

其.next()方法按照规则迭代.value属性,直到它等于1:

完整的定义为:

用for循环迭代生成的迭代器:

在这个过程中,我们并没有构造完整的容器存储这个序列,而是在调用迭代器.next()方法的过程中,不断计算得到下一个值。

对于列表、元组、字典等容器类型来说,为了方便多次循环,每次调用.__iter__()方法时会返回一个新的迭代器:

而对于文件对象来说,通常我们只会迭代文件对象一次,因此它的.__iter__()方法每次会返回同一个迭代器。

3.2.2 生成器

使用类实现自定义类型迭代器比较麻烦,一个更简单的方法是使用生成器(Generator)得到自定义的迭代器。

与类定义不同,生成器使用函数的形式来定义,不过与函数不同,生成器使用yield关键字返回值。

例如,对于Collatz猜想,可以用生成器形式定义如下:

这个生成器在while循环结束前会用yield生成多个值,每次生成的值,相当于迭代器对象.next()方法的返回值;当生成器不能生成出新值时,相当于迭代器对象.next()方法抛出了异常。

使用生成器进行for循环:

生成器是一种特殊的迭代器,我们可以通过.__iter__()方法和.next()方法来验证:

生成器.next()方法的返回值就对应每次yield的返回值。

例如,我们定义一个只有两条yield语句的生成器:

调用两次.next()方法:

In [10]: g.next(), g.next()

Out[10]: (1, 2)

当我们第3次调用.next()方法时,生成器抛出了一个StopIteration异常:

逆序函数也可以用生成器实现:

可以看到,生成器的实现方式要比迭代器更简单。

基于for循环的列表推导式中的内容,也是基于生成器实现的。

例如,对于某个列表推导式:

In [14]: [x for x in range(10)]

Out[14]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

中括号中的for循环推导式是一个生成器对象:

In [15]: (x for x in range(10))

Out[15]: <generator object <genexpr> at 0x000000000487EEE8>

其中,小括号是为了防止歧义,并不是表示元组。推导式生成元组需要显式地调用tuple()函数:

In [16]: tuple(x for x in range(10))

Out[16]: (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)

总之,使用生成器或者迭代器,不需要一次性保存序列的所有值,而只在需要的时候计算序列的下一个值,从而减少程序使用的内存空间。