Python 迭代器链,Python中的迭代器还有另一个重要特性:可以链接多个迭代器,从而编写高效的数据处理“管道”。第一次见到这种模式是在David Beazley的PyCon演讲上,这给我留下了深刻的印象。
利用Python的生成器函数和生成器表达式能很快构建简洁而强大的迭代器链。本节将介绍迭代器链的实际用法,以及如何将其应用到自己的程序中。
快速回顾一下,生成器和生成器表达式是Python中编写迭代器的语法糖。与编写基于类的迭代器相比,这种方式能够省去许多样板代码。
普通函数只会产生一次返回值,而生成器会多次产生结果。可以认为生成器在整个生命周期中能产生值的“流”。
例如,下面的生成器中是一个计数器,每次调用next()
时会产生一个新值,从而生成从1到8的整数值:
def integers():
for i in range(1, 9):
yield i
你可以在Python REPL中运行来确认这个行为:
>>> chain = integers()
>>> list(chain)
[1, 2, 3, 4, 5, 6, 7, 8]
到目前为止并不太有趣,但下面来看一点厉害的。生成器可以相互“连接”,来构建像管道那样工作的高效数据处理算法。
你可以从integers()
生成器中获取值的“流”,将其再次送入另一个生成器。例如,计算传入的每个数的平方,之后再次传出:
def squared(seq):
for i in seq:
yield i * i
这就是“数据管道”或“生成器链”的功能:
>>> chain = squared(integers())
>>> list(chain)
[1, 4, 9, 16, 25, 36, 49, 64]
这个管道能继续添加新的组件。数据仅单向流动,并且每个处理步骤都通过严格定义的接口与其他处理步骤隔离。
这与Unix中的管道工作方式类似。我们也是将一系列过程链接在一起,每个过程的输出直接作为下一个过程的输入。
现在在管道上增加一个步骤,对每个值取负数然后传递给链中的下一个处理步骤:
def negated(seq):
for i in seq:
yield -i
如果重新构建生成器链并在最后加上negated
生成器的话,就会得到下面的结果:
>>> chain = negated(squared(integers()))
>>> list(chain)
[-1, -4, -9, -16, -25, -36, -49, -64]
对于生成器链,我最喜欢的一点是其中每次只处理一个元素。链中的处理步骤之间没有缓冲区。
(1) integers
生成器产生一个值,如3。
(2) 这个值“激活”squared
生成器来处理,得到3 × 3 = 9,并将其传递到下一阶段。
(3) 由squared
生成器产生的平方数立即送入negated
生成器,将其修改为-9并再次yield
。
你可以继续扩展这个生成器链,添加自己的步骤来构建处理管道。生成器链可以高效执行并且很容易修改,因为链中的每一步都是一个单独的生成器函数。
这个处理管道中的每个生成器函数都非常简洁。下面通过一个小技巧来再次简化这个管道的定义,同时不会牺牲可读性:
integers = range(8)
squared = (i * i for i in integers)
negated = (-i for i in squared)
注意,这里将每个处理步骤替换成一个在前一步基础上处理的生成器表达式,等价于前面介绍的生成器链。
>>> negated
<generator object <genexpr> at 0x1098bcb48>
>>> list(negated)
[0, -1, -4, -9, -16, -25, -36, -49]
使用生成器表达式的唯一缺点是不能再使用函数参数进行配置,也不能在同一处理管道中多次重复使用相同的生成器表达式。
不过,你能够在构建这些管道时自由组合和匹配生成器表达式和普通生成器,有助于提高复杂管道的可读性。
关键要点
-
生成器可以链接在一起形成高效且可维护的数据处理管道。
-
互相链接的生成器会逐个处理在链中通过的每个元素。
-
生成器表达式可以用来编写简洁的管道定义,但可能会降低代码的可读性。