深入了解迭代器和生成器
迭代(Iteration) 本章节的主题和迭代密切相关,那什么是迭代呢?在编程中,迭代指的是通过重复执行某个操作,不断获取被迭代对象中的数据。这样的每一次操作就是就是一次迭代。
简而言之,迭代是遍历的一种形式。例如我们之前所学习的 for
循环,它能不断从地从列表、元组、字符串、集合、字典等容器中取出新元素,每次一个元素直至所有元素被取完。这种 for
循环操作就是迭代。
>> for item in [1, 2, 3, 4, 5]:
… print(item)
…
1
2
3
4
5
迭代器(Iterator)
迭代器是具有迭代功能的对象。我们使用迭代器来进行迭代操作。
列表、元组、字符串、集合、字典这些容器之所以能被迭代,是因为对它们调用内置函数 iter() 将返回一个迭代器,这个迭代器可被用于迭代操作。
iter()
的使用方法:
迭代器 = iter(容器)
>>> numbers = [1, 2, 3, 4, 5]
>>> iterator = iter(numbers)
>>> iterator
<list_iterator object at 0x1074f34a8>
上面的「list_iterator」便是列表的迭代器。这个迭代器可用于迭代列表中的所有元素。
要使用迭代器,只需对迭代器调用内置函数 next()
,便可逐一获取其中所有的值。
next()
的使用方法:
值 = next(迭代器)
对于上面的列表迭代器,可以像这样使用它
>>> next(iterator)
1
>>> next(iterator)
2
>>> next(iterator)
3
>>> next(iterator)
4
>>> next(iterator)
5
>>> next(iterator)
Traceback (most recent call last):
File “”, line 1, in
StopIteration
可以看到,每次调用 next()
将依次返回列表中的一个值。直至所有的值被遍历一遍,此时将抛出 StopIteration
异常以表示迭代终止。
for 循环的迭代过程
for 循环的迭代就是通过使用迭代器来完成的。它在背后所做的事情是:
- 对一个容器调用
iter()
函数,获取到该容器的迭代器 - 每次循环时对迭代器调用
next()
函数,以获取一个值 - 若捕获到
StopIteration
异常则结束循环
可迭代(Iterable)对象
并不是所有的对象都可以被 iter()
函数使用。比如整数:
>>> iter(123)
Traceback (most recent call last):
File “”, line 1, in
TypeError: ‘int’ object is not iterable
这里抛出 TypeError 异常,提示 int 对象不是可迭代的。
什么是可迭代(的)?
从表面来看,所有可用于 for 循环的对象是可迭代的,如列表、元组、字符串、集合、字典等容器
从深层来看,定义了 __iter__()
方法的类对象就是可迭代的。当这个类对象被 iter()
函数使用时,将返回一个迭代器对象。如果对象具有__iter__()
方法,则可以说它支持迭代协议。
判断一个已有的对象是否是可迭代的,有两个方法:
通过内置函数 dir()
获取这个对象所有方法,检查是否有 '__iter__'
>>> ‘__iter__’ in dir(list)
True
>>> ‘__iter__’ in dir(int)
False
使用内置函数 isinstance()
判断其是否为 Iterable
的对象
from collections.abc import Iterable
isinstance(对象, Iterable)
>>> from collections.abc import Iterable
>>> isinstance([1, 2, 3], Iterable)
True
自定义迭代器
我们可以自己来定义迭代器类,只要在类中定义 __next__()
和 __iter__()
方法即可。如:
class MyIterator:
def __next__(self):
代码块
def __iter__(self):
return self
我们来写一个迭代器,这个迭代器从 2^0
开始返回 2 的指数幂,至2^10
终止。
class PowerOfTwo:
def __init__(self):
self.exponent = 0 # 将每次的指数记录下来
def __next__(self):
if self.exponent > 10:
raise StopIteration
else:
result = 2 ** self.exponent # 以 2 为底数求指数幂
self.exponent += 1
return result
def __iter__(self):
return self
每次对迭代器使用内置函数 next()
时, next()
将在背后调用迭代器的 __next__()
方法。所以迭代器的重点便是 __next__()
方法的实现。在这个__next__()
方法中,我们将求值时的指数记录在对象属性 self.exponent 中,求值结束时指数加 1,为下次求值做准备。
对于方法 __iter__()
的实现,我们直接返回迭代器对象自身即可。有了这个方法,迭代器对象便是可迭代的,可直接用于for
循环。
扩展:如果对象具有
__iter__()
和__next__()
方法,则可以说它支持迭代器协议。
迭代器 PowerOfTwo 使用示例:
>>> p = PowerOfTwo()
>>> next§
1
>>> next§
2
>>> next§
4
>>> next§
8
>>> next§
16
>>> next§
32
>>> next§
64
>>> next§
128
>>> next§
256
>>> next§
512
>>> next§
1024
>>> next§
Traceback (most recent call last):
File “”, line 1, in
File “”, line 6, in next
StopIteration
这个迭代器当然也可用于 for 循环:
>>> p = PowerOfTwo()
>>> for item in p:
… print(item)
…
1
2
4
8
16
32
64
128
256
512
1024
迭代器的好处
- 一方面,迭代器可以提供迭代功能,当我们需要逐一获取数据集合中的数据时,使用迭代器可以达成这个目的
- 另一方面,数据的存储是需要占用内存的,数据量越大所占用的内存就越多。如果我们使用列表这样的结构来保存大批量的数据,并且数据使用频率不高的话,就十分浪费资源了。而迭代器可以不保存数据,它的数据可以在需要时被计算出来(这一特性也叫做惰性计算)。在合适的些场景下使用迭代器可以节省内存资源。
生成器(Generator)
刚才我们自定义了迭代器,其实创建迭代器还有另一种方式,就是使用生成器。
生成器是一个函数,这个函数的特殊之处在于它的 return
语句被 yield
语句替代。
如刚才用于生成 2 的指数幂的迭代器,可以通过生成器来实现:
def power_of_two():
for exponent in range(11): # range(11) 表示左开右闭区间 [0, 11),不包含 11
yield 2 ** exponent # 以 2 为底数求指数幂
生成器使用方法:
p = power_of_two() # 以函数调用的方式创建生成器对象
next(p) # 同样使用 next() 来取值
生成器的关键在于 yield
语句。yield
语句的作用和 return
语句有几分相似,都可以将结果返回。不同在于,生成器函数执行至 yield
语句,返回结果的同时记录下函数内的状态,下次执行这个生成器函数,将从上次退出的位置(yield 的下一句代码)继续执行。当生成器函数中的所有代码被执行完毕时,自动抛出 StopIteration
异常。
我们可以看到,生成器的用法和迭代器相似,都使用 next()
来进行迭代。这是因为生成器其实就是创建迭代器的便捷方法,生产器会在背后自动定义 __iter__()
和 __next__()
方法。
生成器表达式(Generator Expression)
可以用一种非常简便的方式来创建生成器,就是通过生成器表达式。生成器的写法非常简单,但是灵活性也有限,所能表达的内容相对简单。
生成器表达式的写法如下:
生成器 = (针对项的操作 for 项 in 可迭代对象)
如:
>>> letters = (item for item in ‘abc’)
>>> letters
<generator object at 0x1074a8228>
>>> next(letters)
‘a’
>>> next(letters)
‘b’
>>> next(letters)
‘c’
>>> letters = (i.upper() * 2 for i in ‘abc’)
>>> next(letters)
‘AA’
>>> next(letters)
‘BB’
>>> next(letters)
‘CC’