迭代是 Python 最强大的功能之一,这一篇文章将介绍 Python 迭代器与生成器相关的编码技巧。
手动遍历迭代器
问题
想要手动遍历迭代器,但是不想使用 for 循环。
解决方案
- 为了手动地遍历迭代器,使用
next()
函数并在代码中捕获 StopIteration 异常 next()
函数支持以指定值来标记结尾
示例
1 | def manual_iter(): |
1 | 1, 2, 3, 4] a = [ |
代理迭代
问题
当构建了一个自定义容器时,里面包含了列表、元组或其他可迭代对象。需要再这个新的容器对象上执行迭代操作。
解决方案
- python 的迭代器协议要求在
可迭代对象
上调用iter()
函数时,将返回一个迭代器
(iterator) iter(s)
等效于s.__iter__()
,因此可迭代对象
需要实现__iter__()
方法,返回一个迭代器
迭代器
实现了__next__()
方法,因此可以在迭代器上调用next()
函数next(i)
等效于i.__next__()
示例
1 | class Node: |
使用生成器创建新的迭代器模式
问题
想实现一个自定义迭代器模式,和内置的 range()
、reserved()
不一样
解决方案
当想要实现一种新的迭代模式,可以通过生成器函数来定义它:
- 在 Python 中,一个函数包含
yield
语句,就是生成器函数
。调用生成器函数
将返回一个生成器
生成器
是迭代器,支持迭代协议,因此可以在生成器
上调用next()
函数。此时将会执行函数体,直至遇到yield
语句,返回所产生的值,并在该位置停止。当再次调用next()
函数时,将从函数停止位置恢复执行,依次类推。最终函数执行完毕并返回时,将抛出 StopIteration 异常
示例
1 | def frange(start, stop, increment): |
1 | def countdown(n): |
实现迭代器协议
问题
想让自定义类型支持迭代操作,用最简单的方法实现迭代协议。
解决方案
- Python 的迭代器协议要去:类型提供了
__iter__()
方法并返回一个迭代器。该迭代器实现了__next__()
方法并且通过StopIteration
异常标识迭代的完成 - 在自定义类型实现迭代最简单的方式就是使用一个
生成器函数
,当把自定义类型的__iter__()
方法实现为生成器函数
时,该类型就天然支持迭代器协议 - 更复杂的方式则是自己实现这套迭代器协议。此时可能需要在迭代处理过程中维护大量状态信息
示例
1 | class Node: |
1 | class Node: |
反向迭代
问题
想反方向迭代一个序列。
解决方案
- 可以使用内置的
reversed()
函数 - 反向迭代仅仅当对象大小可以预先确定,或者对象实现了
__reversed__()
方法 - 当两者都不符合时,必须先将对象转换为一个列表才行。例如迭代一个文件对象时
- 因此对于自定义类型,只要实现了
__reversed__()
方法,也就可以支持反向迭代 - 定义反向迭代可以使代码非常高效,因为此时不需要先将数据填充到一个列表中,然后再去反向迭代这个列表
示例
1 | 1, 2, 3, 4] a = [ |
1 | open('somefile') f = |
1 | class CountDown: |
带有外部状态的生成器函数
问题
在调用生成器时,需要暴露一些给用户使用的外部状态值。
解决方案
- 生成器逻辑不一定要真正地实现为函数,也可以将它实现为一个类,然后把
__iter__
函数定义为生成器函数 - 此时该类型的对象支持迭代器协议,同时可以访问该类型对象的内部属性值,从而获得这些状态值
示例
1 | class linehistory: |
迭代器切片
问题
想要在迭代器上生成切片对象。
解决方案
- 标准的切片操作不能应用于迭代器,因为它们的长度事先不知道
itertools.islice()
可以用于在迭代器和生成器上做切片操作itertools.islice()
函数会消耗掉传入迭代器中的数据:它会首先丢弃从首元素
到开始索引位置
之间的所有元素,之后才逐个返回元素,直到结束索引位置
示例
1 | def count(n): |
跳过可迭代对象的开始部分
问题
想要遍历一个可迭代对象,但是开始的某些元素并不感兴趣,需要跳过。
解决方案
itertools.dropwhile()
函数可以实现该任务,该函数接收一个函数对象和一个可迭代对象,它会返回一个迭代器对象,丢弃原有序列中直到函数返回 False
之前的所有元素,然后开始正常返回元素- 如果你明确知道跳过元素的序列,也可以使用
itertools.islice()
,此时将结束索引设置为 None 即可。
示例
如下代码跳过文件开始的 #
:
1 | from itertools import dropwhile |
如果不使用 dropwhile()
,代码会复杂一些:
1 | with open('somefile') as f: |
1 | from itertools import islice |
排列组合的迭代
问题
如果想迭代一个集合中元素的所有可能的排列或组合。
解决方案
itertools.permutations()
接收一个集合并产生一个元组序列,每个元组由集合中的所有元素的一个可能排列组成- 如果想得到指定长度的所有排列,可以指定一个可选的长度参数
- 使用
itertools.combinations()
可以得到输入集合中元素的所有组合 itertools.combinations_with_replacement()
允许同一个元素被选择多次- 如果碰到复杂的迭代问题时,可以先看看
itertools
模块,看看有没有解决方案
示例
1 | 'a', 'b', 'c'] items = [ |
序列上索引值迭代
问题
如果想在迭代一个序列的同时,跟踪正在被处理元素的索引。
解决方案
enumerate()
函数可以解决该问题enumerate()
函数返回的是一个enumerate
对象实例,它是一个迭代器,返回连续的包含一个计数值和一个值的元组enumberate()
函数还可以接受一个start
参数,用于指定计数索引的起始值
示例
1 | 'a', 'b', 'c', 'd'] l = [ |
1 | def parse_data(filename): |
同时迭代多个序列
问题
如果你想同时迭代多个序列,每次分别从一个序列中获取一个元素。
解决方案
- 为了同时迭代多个序列,可以使用
zip()
函数,zip(a, b)
会生成一个可返回元组(x, y)
的迭代器,其中 x 来自 a、y 来自 b。一旦某个序列到达结尾,则迭代结束因此迭代长度和参数中最短序列长度一致。如果想实现按最长序列进行迭代,可以使用itertools.zip_longest()
函数
示例
1 | 1, 2, 3, 4, 5] xpts = [ |
1 | 1, 2, 3] a = [ |
不同集合上元素的迭代
问题
如果你想要在多个对象上执行相同的操作,但是这些对象在不同的容器中,如何避免写重复循环。
解决方案
itertools.chain()
方法可以接受一个可迭代对象列表作为输入,并返回一个迭代器。通过该迭代器可以依次连续地返回每个可迭代对象中的元素itertools.chain()
非常适合对不同集合(不同可迭代对象类型)中的所有元素执行某些操作,它比使用多个单独的循环更加优雅
示例
1 | 1, 2, 3, 4] a = [ |
1 | 1, 2, 3) aset = ( |
创建数据处理管道
问题
想以数据管道(类似 Unix 管道)的方式迭代处理数据。
解决方案
- 以管道方式处理数据可以用来解决
大量数据的一次性处理问题
- 生成器函数是一个实现管道机制的好方法。重点需要理解:
yield
语句是数据的生产者,而for 循环语句
则是数据的消费者。当生成器被连接在一起时,每个 yield 结果会作为一个单独的数据元素传递给迭代处理管道的下一阶段 - 这种实现方式的优点是:每个生成器函数都很小并且都是独立的,便于维护与扩展
yield from it
将操作代理到it
迭代器上,并简单地返回生成器 it 所产生的值,
示例
1 | import fnmatch |
展开嵌套的序列
问题
想将一个多层嵌套的序列展开成一个单层列表。
解决方案
- 语句
yield from
在你想在生成器中调用其他生成器作为子例程时非常有用 - 如果不使用
yield from
,你就得多写一个for 循环
yield from
在涉及到基于协程和生成器的并发编程中也扮演重要角色
示例
1 | #!/usr/bin/env python3 |
顺序迭代合并后的排序迭代对象
问题
你有一系列排序序列,想要将它们合并后得到一个排序序列并在上面迭代遍历。
解决方案
- 使用
heapq.merge()
可以对多个已排序序列进行合并,而且它返回一个生成器对象,这就意味着它并不会马上将所有序列都读取到内存中,因此在非常长的序列中使用该函数,也不会有太大开销 heapq.merge()
所输入的序列必须是排过序的。它仅仅检查所有序列的开始部分并返回最小的那个,该过程会持续直到所有输入序列中的元素被遍历完成
示例
1 | >>> import heapq |
迭代器替代 while 无限循环
问题
如果你在代码中使用 while
循环来处理数据,然后当满足某个条件时在推出循环。这种编码模式能否有迭代器来实现呢?
解决方案
iter()
函数一个鲜为人知的特性是它接受一个可选的callable
对象和一个标记结尾的值作为输入参数,它会创建一个迭代器,该迭代器不断调用callable()
对象,直到返回值和标记值相等,此时表示迭代结束- 这种方法对于一些特定的、会被重复调用的函数很有效果
示例
1 | 8192 CHUNKSIZE = |
1 | def reader2(s): |