Random Tech Thoughts

The title above is not random

Python 的 Iterator Protocol 和 Generator

前一篇文章最后的代码效率很差。由于使用 list 保存临时的计算结果,所有文件内容会同时读入内存,其构建的管道类似下图所示:

list-pipelist 中的元素是先通过一个过滤器,存起来,再通过下一个过滤器。这样做的坏处是

  1. 浪费内存
  2. 浪费时间,来回的在临时 list 中进行存取需要时间,另外临时 list 的内存分配,垃圾收集都会增加时间开销

我们希望的管道其实如下:

generator-pipelist 中的每个元素应该一次性通过所有的 filter,只在最后存在一个 list 中。更理想的情况下,一开始的 list 也不应该出现。对于像 Haskell 这样默认使用 lazy evaluation 的语言来说,前面的函数连续调用默认行为就是如此。只有当管道末端需要一个元素时才会从管道头读取一个元素,经过所有过滤器处理以后得到结果,如果没有读取第二个元素,则管道头的其他元素根本不会被处理。

要在程序中使用管道的风格又必须要避免临时数据的产生,然而 python 中没有直接对 lazy evaluation 提供支持,好在 python 有 iterator protocol 和 generator。

首先介绍 python 的 iterator protocol,了解它才知道 for 语句进行遍历时究竟做了些什么。iterator protocol 使得我们可以对不同的容器(其实是任何支持遍历操作的对象,不一定是用来存放数据的容器)使用相同的方式(for 语句)进行遍历。容器可以是 list, dictionary 或者是其他用户定义的数据结构,它只需要实现 iter()方法,返回一个 iterator object 即可。真正关心如何遍历的是 iterator object,它的两个方法构成了 iterator protocol

  1. __iter__() 返回自身
  2. next() 返回容器中的下一个对象,没有更多对象时应 raise StopIteration,一旦抛出此异常,后续的调用一定也必须抛出此异常。

偷懒起见,直接拿 Beazley 的幻灯片里的例子来说明:

1
2
3
4
5
6
7
8
9
10
 class countdown(object):
    "iterator object"
    def __init__(self, start):
        self.count = start
    def __iter__(self):
        return self
    def next(self):
        if self.count

for 语句首先对要遍历的对象调用 __iter__() 方法得到 iterator object,然后对 iterator object 不停调用 next() 方法直到遇到 "StopIteration" 异常为止。由此, 下面代码中的 for 语句和 while 循环等价:
1
2
3
4
5
6
7
8
9
10
11
12
cd = countdown(2)

for i in cd:
   print i

_iter = cd.__iter__(cd) # Get iterator object
while 1:
    try:
        x = _iter.next() # Get next item
        print x
    except StopIteration: # No more items
        break

由于 iterator object 的 iter() 返回自身,因此 iterator object 也可以用在 for 语句中。实际上可以把 for 语句看成是一种语法糖,但它是一种重要的语法糖,因为它大大简化了程序的编写,看看上面例子比较下就知道了。

为了使得容器支持遍历操作而要另外定义一个 iterator 类,这跟 C++ 的 STL 有些类似。使用 generator object 可以不需要另外定义 iterator 类从而更方便的实现 iteration protocol。具体做法如下:

1
2
3
4
5
6
7
8
9
 class countdown():
    "container"
    def __init__(self, start):
        self.count = start
    def __iter__(self):
        it = self.count
        while it > 0:
            yield it
            it -= 1

一旦在函数定义中使用 yield,这个函数就成为 generator function 而不是普通的函数。 generator function 被调用时会返回 generator iterator,简称 generator。调用 generator 的 next() 方法时,generator function 的函数体会执行,当遇到 yield 时,generator 的状态会冻结,而当前值被返回。

lazy evaluation, generator 的其他用处

利用 generator 可以模拟出 lazy evaluation 来。lazy evaluation 我觉得最有用的地方在于,

  1. 使用管道风格的代码处理数据时,避免产生大量临时的结果,提高性能
  2. 处理无限长度的数据
  3. 避免不必要的求值

但 generator 不光可以用来处理数据,Beazley 的幻灯片里提到 networking, threads, co-routines 等都可以用 generator 来处理。

Comments