前一篇文章最后的代码效率很差。由于使用 list 保存临时的计算结果,所有文件内容会同时读入内存,其构建的管道类似下图所示:
list 中的元素是先通过一个过滤器,存起来,再通过下一个过滤器。这样做的坏处是
- 浪费内存
- 浪费时间,来回的在临时 list 中进行存取需要时间,另外临时 list 的内存分配,垃圾收集都会增加时间开销
我们希望的管道其实如下:
list 中的每个元素应该一次性通过所有的 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。
- __iter__() 返回自身
- next() 返回容器中的下一个对象,没有更多对象时应 raise StopIteration,一旦抛出此异常,后续的调用一定也必须抛出此异常。
偷懒起见,直接拿 Beazley 的幻灯片里的例子来说明:
1 2 3 4 5 6 7 8 9 10 |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
|
由于 iterator object 的 iter() 返回自身,因此 iterator object 也可以用在 for 语句中。实际上可以把 for 语句看成是一种语法糖,但它是一种重要的语法糖,因为它大大简化了程序的编写,看看上面例子比较下就知道了。
为了使得容器支持遍历操作而要另外定义一个 iterator 类,这跟 C++ 的 STL 有些类似。使用 generator object 可以不需要另外定义 iterator 类从而更方便的实现 iteration protocol。具体做法如下:
1 2 3 4 5 6 7 8 9 |
|
一旦在函数定义中使用 yield,这个函数就成为 generator function 而不是普通的函数。 generator function 被调用时会返回 generator iterator,简称 generator。调用 generator 的 next() 方法时,generator function 的函数体会执行,当遇到 yield 时,generator 的状态会冻结,而当前值被返回。
lazy evaluation, generator 的其他用处
利用 generator 可以模拟出 lazy evaluation 来。lazy evaluation 我觉得最有用的地方在于,
- 使用管道风格的代码处理数据时,避免产生大量临时的结果,提高性能
- 处理无限长度的数据
- 避免不必要的求值
但 generator 不光可以用来处理数据,Beazley 的幻灯片里提到 networking, threads, co-routines 等都可以用 generator 来处理。