简介
在以前一篇讲述iterator的文章里,我提到过通过一种循环遍历的方式去访问一个python对象需要具备的特性。在那里,我们要实现一个iterator的时候,要定义一个可以访问的迭代器__iter__,同时也需要定义一个next()方法用来获取后面的一个元素。这是一种遍历集合元素的方式。这里主要依赖的是我们在一个集合元素已经取得的情况下。使用iterator的时候,我又碰到一个看起来很近似的东西,generator。generator有什么作用呢?为什么要专门折腾一个generator出来?我们一一讨论过来。
Generator和Iterator的比较
我们来看一段很简单的代码:
def numlist(n): ... while(n < 10): ... yield n ... n += 1
这里有一个看似有点怪异的语法,yield n。如果我们要使用这个方法,该怎么用呢?
>>> for i in numlist(0): ... print i, ... 0 1 2 3 4 5 6 7 8 9
从用法来看,这里好像是numlist()作为集合来遍历。如果我们了解iterator的话,我们完全也可以用iterator实现一个来达到同样的目的啊,比如说我们实现一个有同样输出的iterator如下:
class numlist: def __init__(self, start): self.count = start def __iter__(self): return self def next(self): if self.count >= 10: raise StopIteration r = self.count self.count += 1 return r
如果我们用如下的代码来遍历的话,也会得到如下的结果:
>>> for i in numlist(0): ... print i, ... 0 1 2 3 4 5 6 7 8 9
从表面上看起来,他们两者都返回类似集合的东西,好像没什么区别。我们深入的分析一下看看。我们先来看看前面定义的generator部分:
>>> from genlist import numlist >>> x = numlist(0) >>> x <generator object numlist at 0x7f217108a780>
输出的结果显示numlist方法返回的结果是一个generator。
而如果我们引用那个iterator的实现,则结果如下:
>>> from countup import numlist >>> x = numlist(0) >>> x <countup.numlist instance at 0x7f17e42a1440>
这里显示的x是一个numlist的实例对象。而如果我们进一步去深究的,这个x是什么详细的定义呢?我们在python命令行里输入help(x):
class numlist | Methods defined here: | | __init__(self, start) | | __iter__(self) | | next(self)
从结构上来说,这里就是我们前面讨论过的典型的iterator。我们再来看generator那部分的:
numlist = class generator(object) | Methods defined here: | | __getattribute__(...) | x.__getattribute__('name') <==> x.name | | __iter__(...) | x.__iter__() <==> iter(x) | | __repr__(...) | x.__repr__() <==> repr(x) | | close(...) | close(arg) -> raise GeneratorExit inside generator. | | next(...) | x.next() -> the next value, or raise StopIteration | | send(...) | send(arg) -> send 'arg' into generator, | return next yielded value or raise StopIteration. | | throw(...) | throw(typ[,val[,tb]]) -> raise exception in generator, | return next yielded value or raise StopIteration. | | ---------------------------------------------------------------------- | Data descriptors defined here: | | gi_code | | gi_frame | | gi_running
和前面的iterator不一样,这里有一个生成的generator类,然后在类里多了close, send等几个方法。看到这里的时候,我们发现generator里完全有和iterator一样的功能,比如通过next()方法来遍历他们。但是我们在前面定义的时候更加简练一些。
Generator和list的比较
在前面这一部分我们比对了generator和iterator的结构形式。发现generator的功能似乎涵盖了iterator。除了我们前面实现代码的一种定义方式,我们还有一种实现generator的形式,它看起来和list comprehension很相似。我们看如下的代码:
>>> a = [1, 2, 3, 4] >>> b = (2*x for x in a) >>> b <generator object <genexpr> at 0x16e5a00> >>> for i in b: print i, ... 2 4 6 8
在前面的代码里,如果我们将b定义成[2*x for x in a],得到的结果则不一样了:
>>> c = [2*x for x in a] >>> c [2, 4, 6, 8] >>> type(c) <type 'list'>
仅仅是一个括号的区别,返回的结果就完全不同。虽然我们也可以用generator来作为遍历的结果。那么generator和list遍历的结果有什么不一样呢?
在前面的代码里,我们执行第一次循环generator的时候,输出一组数字,可是这个generator只能使用一次,而list却是可以使用无数次的:
>>> for i in b: print i, ... 2 4 6 8 >>> for i in b: print i, ... >>> for i in a: print i, ... 1 2 3 4 >>> for i in a: print i, ... 1 2 3 4
看来这个generator就像个一次性消费品,定义起来简单,但是一次就用完了。这么看来,generator还比较麻烦,还不如list呢。其实generator还有一个优点,就是我们一般遍历数据的时候,都是需要已经构造好一个集合了,然后再去遍历它们。这样如果在数据集合比较大的时候就变得不可行了。而generator并不预先构造一个集合。每次我们循环遍历的时候,循环一次就取一个数据。因为这么个特性,它不需要将所有的集合数据都保存下来。在一些大规模数据的处理的情况下,它显得更加高效率。
Generator的作用和思想
在前面的描述中generator是一个可以无限取数据的流水线,对它的使用就像是操作一个集合一样,但是一旦启用了之后它从读取数据到所有数据操作完毕,它只能用一次。在一些场景中,我们可能需要对一些数据做多个步骤的处理。如果我们从一开始对数据进行操作的时候就使用generator的话,会发现后面都必须要利用它来做进一步的组合。我们来看一个示例。
假设我们有一个log文件,那里记录了web服务器数据访问的信息,它里面保存的信息是如下格式的:
81.107.39.38 - ... "GET /ply/ply.html HTTP/1.1" 200 97238
在一行最末尾的地方记录的是一次访问操作的时候传输的数据量大小,单位为字节。最后一位可以为一个数字或者为一个-字符。如果我们需要统计里面所有请求的数据量,那么该怎么来实现呢?下面是一种是我们想到的传统实现方法:
wwwlog = open("access-log") total = 0 for line in wwwlog: bytestr = line.rsplit(None,1)[1] if bytestr != '-': total += int(bytestr) print "Total", total
这里的代码思路比较简单,首先我们打开这个log文件,然后读取文件的每一行,再将取到的没一行里的数据量相加,最后得到总数据传数量。
如果我们结合前面generator的用法来看,这里还有一种写法:
wwwlog = open("access-log") bytecolumn = (line.rsplit(None,1)[1] for line in wwwlog) bytes = (int(x) for x in bytecolumn if x != '-') print "Total", sum(bytes)
我们取每一行里的数据量部分是返回一个generator,然后每次针对这个部分解析数值也是返回一个generator,最后再通过一个sum()方法来统计所有的和。在后面这种写法里,我们更多的像是在声明说要取哪些数据,然后每一步是在原来generator的基础上再套generator。有点流水线套流水线的味道。和前面的比起来,这种写法更加简单和直观。代码行数都更少一些。
总结
generator的定义可以通过yield n或者类似于list comprehension的方法来构造。它本身不构造数据列表,因此可以处理理论上无穷的数据而不会导致机器的存储资源耗尽。generator类似于一个车间生产的流水线,每次需要用产品的时候才临时从那里取一个,然后这个流水线就停在那里等待下一次取操作。我们可以在实际应用中将多个generator串在一起来用,这一点和unix设计思想里pipeline的思想居然是惊人的一致。另外,如果我们使用generator写代码来表述一些处理的过程时,体现出来的更多是一种声明式的写法,这和函数式编程的思想居然有如此紧密的联系。关于这些关系值得以后文章里进一步的探讨。
参考材料
http://www.dabeaz.com/generators/