Chapter 14 Iterables, Iterators, and Generators
迭代器模式:惰性获取数据项的方式。按需一次获取一个数据项,可处理内存中放不下的数据集。
所有生成器都是迭代器,生成器完全实现了迭代器接口。但根据定义,迭代器用于从集合中取出元素,而生成器用于生成元素。
14.1 Sentence 类第 1 版:单词序列
- 解释器迭代对象时会调用
iter(x),其优先调用对象的__iter__方法(返回迭代器),若没有则尝试使用__getitem__按顺序获取元素,以此创建一个迭代器; __iter__方法是Iterable抽象基类的钩子——无需注册即可成为子类。因此通过__getitem__实现的可迭代对象不是Iterable的子类;- 检查是否可迭代的最准确方法是调用
iter(x)函数,并尝试捕获异常。
14.2 可迭代的对象与迭代器的对比
- 可迭代的对象:如果实现了能返回迭代器的
__iter__方法,则对象是可迭代的; - 将
for循环展开,展示迭代器的机制——next(it)与StopIteration异常:
s = 'ABC'
it = iter(s) # construct an iterator
while True:
try:
print(next(it)) # use next(it) to get the next element
except StopIteration: # StopIteration Error will be raised in the end
del it # release the iterator reference
break
Iterator继承自Iterable类:Iterable定义了抽象方法__iter__(可迭代对象可以调用iter()函数);Iterator将__iter__定义为返回自身,新增了抽象方法__next__用于迭代,返回下个元素或抛出异常;__iter__和__next__方法是Iterable抽象基类的钩子——实现了即为迭代器。特别地,迭代器也是可迭代的。
14.3 Sentence 类第 2 版:典型的迭代器
- 根据迭代器设计模式,为了支持多种遍历,必须能从同一个可迭代的实例中获取多个独立的迭代器,各自维护自身的内部状态。因此需要设计独立的迭代器类型,不能是它本身;
- 更符合 Python 习惯的方式是使用生成器函数来替代自定义的迭代器类型。
14.4 Sentence 类第 3 版:生成器函数
- 可以将
__iter__定义为返回生成器对象——每次调用时会自动创建,这里的__iter__方法是生成器函数:
def __iter__(self):
for word in self.words: # <1>
yield word # <2>
return # <3>
- 只要函数定义体中有
yield关键字,该函数就是生成器函数。调用时会返回生成器对象,包装了生成器函数的定义体。它实现了迭代器接口,迭代方式与前面相同:
def gen_123():
yield 1
yield 2
yield 3
for i in gen_123():
print(i) # 1 2 3
- 调用
next()函数时,生成器函数向前执行,直到函数定义体中的下一个yield语句,返回产出的值,并在函数定义体当前位置暂停。定义体执行完毕后抛出StopIteration异常; - 在 Python 3.3 之后,如果生成器函数有返回值,则可以通过异常对象获取值。
14.5 Sentence 类第 4 版:惰性实现
懒惰求值(Lazy Evaluation)与及早求值(Eager Evaluation)是编程理论的技术术语。
- 小技巧:
re.finditer是re.findall的懒惰版本,返回一个生成器,可节省大量内存。其生成的re.MatchObject实例可以利用.group()方法提取具体文本。
14.6 Sentence 类第 5 版:生成器表达式
- 生成器表达式的效果与生成器函数完全等价,其得到的值即为生成器对象;
- 生成器可以嵌套使用,只有真正循环迭代时,最内部生成器的函数体才会执行(惰性)。
14.7 何时使用生成器表达式
- 生成器表达式语法更简洁;生成器函数更灵活,可以用多语句实现复杂逻辑,具有函数名称可以重用,也可以作为协程使用;
- 如果函数只有一个实参,且该实参为生成器表达式,则可以省略一对括号;
- 迭代器与生成器的实际运用总结:
- 如
range(5),[1,2]等可迭代对象不是迭代器,可以通过iter(x)得到迭代器。内置类型的迭代器都有特殊的类型名,如range_iterator、list_iterator等; - 生成器是迭代器,是可迭代对象。生成器可以通过生成器表达式直接得到,也可以通过调用生成器函数得到;
for循环使用的是迭代器。
- 如
14.8 示例:等差数列生成器
itertools模块包括许多生成器函数:itertools.count(begin, step)返回的生成器会生成无穷的等差数列;itertools.takewhile(predicate, gen)返回一个新的生成器,其使用gen生成元素,直到predicate返回False为止;
- 小技巧:
type()返回类对象,可以以此作为构造方法。因此可以用type(B)(A)的语句将A的类型强制转换为B的类型;- 处理浮点数的运算时,注意精度损失累积的问题,尽量避免浮点数的累加。
14.9 标准库中的生成器函数
以下得到的生成器中,若输入可迭代对象长度不同,会在某个可迭代对象到头后停止。
以下的生成器函数基本都可以组合在一起嵌套使用。
用于限制过滤的生成器函数
itertools.compress(it, selector_it):产出selector_it对应位置为真的it中元素;itertools.dropwhile(predicate, it):先跳过predicate为真的元素,后产出所有剩下的的元素,不再检查;itertools.takewhile(predicate, it):产出predicate为真的元素,不满足时立即停止;filter(predicate, it):产出predicate为真的元素;若predicate=None,则产出所有it中的真值元素;itertools.filterfalse(predicate, it):与filter相反;itertools.islice(it, stop) / (it, start, stop, step=1):产出切片,是惰性操作。
用于映射的生成器函数
itertools.accumulate(it, [func]):累加,默认为求和,但是会在每一位记录当前的累加结果,返回一个生成器,而非只返回一个值;enumerate(iterable, start=0):产出元组(index, item);map(func, it1[, it2, ..., itN]):it作为参数传给func,可接受多个参数,并行处理;itertools.starmap(func, it):对it产出的每个元素iit,将*iit作为参数传给func;starmap(lambda a, b: b/a, enumerate(accumulate(sample), 1)))可以用来求平均值。
用于合并的生成器函数
itertools.chain(it1, ..., itN):无缝连接各可迭代对象;itertools.chain.from_iterable(it):it产出可迭代对象,将它们无缝连接;zip(it1, ..., itN):产出由 N 个元素构成的元组,直到一个可迭代对象到头;itertools.zip_longest(it1, ..., itN, fillvalue=None):同zip,用fillvalue填充;itertools.product(it1, ..., itN, repeat=1):计算笛卡儿积,生成由N*repeat个元素构成的元组。repeat表示前面的N个可迭代对象整体重复几次。
用于扩展输入的生成器函数
itertools.count(start=0, step=1):不断产出等差数列;itertools.repeat(item, [times]):不断产出指定元素,直到指定次数(若有)。常用于给map函数提供固定参数;itertools.cycle(it):产出it中各元素,存储副本,然后按顺序重复不断地循环产出;itertools.combinations(it, out_len):选择组合,从it中选取out_len个不同元素,产出所有组合;itertools.combinations_with_replacement(it, out_len):可以选取相同元素;itertools.permutations(it, out_len=None):选择排列,从it中选取out_len个不同元素并排列,产出所有的排列;- 以上三个生成器函数与
product构成组合学生成器。
- 以上三个生成器函数与
用于分组排列的生成器函数
reversed(seq):只接受序列或实现__reversed__的对象,倒序产出seq中元素;itertools.groupby(it, key=None):产出不同的(key, group),根据元素的key将it中的元素分组,分组得到的元素通过各生成器group得到。注意:使用前需要先用分组标准排序或分组,也就是说该函数做的只是划分;itertools.tee(it, n=2):产出n个生成器,分别可以单独产出输入对象中的元素。
14.10 Python 3.3 中新出现的句法:yield from
yield from语句用于生成器函数中,可从后面的生成器中生成元素,代替嵌套循环;- 它还创建通道,把内层生成器和外层生成器的客户端联系起来。
14.11 可迭代的归约函数
- 归约函数:接受可迭代对象,返回单个结果。包含以下函数:
all(it):全为真时返回True。it为空时返回True。具有短路特性,会停止迭代;any(it):存在真时返回True。it为空时返回False。具有短路特性,会停止迭代;max/min(it, [key=], [default=]):返回最大/小值,键key。it为空时返回default;sum(it, start=0):求总和,初始值为start;math.fsum(it, start=0):同sum,用于浮点数求和,提高精度;functools.reduce(func, it, [initial]):归约函数,两两求结果。若有initial,则第一次计算initial和it首元素的计算结果;
sorted(it, [key=], [reversed=False])可以接受任意可迭代对象,返回列表。
14.12 深入分析 iter 函数
iter(func, value)会使用可调用对象和标记符创建迭代器(callable_iterator对象)(从结果上来看,这与iter函数的一般用途是一致的),它不断调用func()产出值,直到值为value时,抛出StopIteration异常;iter的用途:逐行读取文件,直到遇到空行或文件末尾:
with open('data.txt') as fp:
for line in iter(fp.readline, '\n'):
process_line(line)
14.13 案例分析:在数据库转换工具中使用生成器
- 在文件读取处理的过程中,如果有多种读取、处理的方式,为了减小耦合性,可以使用生成器的方式读取并产出数据,再调用不同方法处理,把主要的选择逻辑在主函数中实现:
def main():
reader = reader_gen1() # reader_gen1 is a generator function
# reader = reader_gen2() # reader_gen2 is a generator function
processer = process1 # process1 is a function
# processer = process2 # process2 is a function
for data in reader: # main part
processer(data)
def reader_gen1():
yield 1
def reader_gen2():
yield 2
def process1():
pass
def process2():
pass
- 使用这种方式解耦读和写,支持交叉读写,无需一次性读取,可以处理任意大小的文件。这样相当于把文本内容看成数据流,随时读取处理。
14.14 把生成器当成协程
- 生成器对象的
.send()方法不仅使生成器继续执行到下一个yield语句,还支持使用生成器的客户通过方法的参数把数据发给自己,成为yield表达式的值。它允许在客户代码和生成器之间双向交换数据,这改变了生成器的本性,变为协程; - 协程与迭代无关,会在第 16 章具体讨论。