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