深入了解 Generator

原文作者:David Beazley
中文编译:Tony (digitalsatori)

第一部分:Iterators 和 Generators简介

Iteration 枚举

  • 众所周知Python有一个“for”语句
  • 你可以用它来在一组对象上做循环操作
>>> for x in [1, 4, 5, 10]:
...     print x,
...
1 4 5 10
>>>
  • 你不仅仅可以对list做循环枚举操作,还可以对很多不同类型的对象做相同的操作

对Dict的枚举

  • 对字典进行循环枚举操作,会得到该字典的keys
>>> prices = { 'GOOG' : 490.10,
...            'AAPL' : 145.23,
...            'YHOO' : 21.71 }
...
>>> for key in prices:
...     print key
...
YHOO
GOOG
AAPL
>>>

对String的枚举

  • 对字符串进行循环枚举操作,会得到字符
>>> s = "Yow!"
>>> for c in s:
...     print c
...
Y
o
w
!
>>>

对文件的枚举

  • 对文件进行循环枚举操作,会得到文本行
>>> for line in open("real.txt"):
...     print line,
...
    Real Programmers write in FORTRAN
Maybe they do now,
in this decadent era of
Lite beer, hand calculators, and "user-friendly" software
but back in the Good Old Days,
when the term "software" sounded funny
and Real Computers were made out of drums and vacuum tubesReal Programmers wrote in machine code.
Not FORTRAN. Not RATFOR. Not, even, assembly language.
Machine Code.
Raw, unadorned, inscrutable hexadecimal numbers.
Directly.

“消费”(Consume) 可枚举对象

  • 很多函数可以消费 可枚举 对象, 比如
    • 消减(Reductions):

      sum(s), min(s), max(s)

    • 构造器(Constructors):

      list(s), tuple(s), set(s), dict(s)

    • in 操作符

      item in s

    • 以及函数库中众多的其他函数

枚举协议(Iteration Protocol)

  • 能对不同的对象进行枚举操作的原因是有一个特别的协议
>>> items = [1, 4, 5]
>>> it = iter(items)
>>> it.next()
1
>>> it.next()
4
>>> it.next()
5
>>> it.next()
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
StopIteration
>>>
  • 深入了解for语句
for x in obj:
  # statements
  • 其本质是
_iter = iter(obj)             # 获取枚举器(iterator)对象
while 1:
   try:
      x = _iter.next()        # 获取下一个对象
   except StopIteration:      # 没有更多的对象了
      break
   # statements
    ...
  • 任何支持 iter()next() 的对象都被称为 可枚举 (iterable) 对象

如何支持枚举

  • 用户自定义对象可以支持枚举操作
  • 比如:倒计数...
>>> for x in countdown(10):
...     print x,
...
10 9 8 7 6 5 4 3 2 1
>>>
  • 要做到这点,你只要让该对象拥有 __iter__()next()
  • 比如:
class countdown(object):
   def __init__(self, start):
       self.count = start
   def __iter__(self):
       return self
   def next(self):
       if self.count <=0:
           raise StopIteration
       r = self.count
       self.count -= 1
       return r

枚举示例

  • 应用举例:
>>> c = countdown(5)
>>> for i in c:
...     print i,
...
...
5 4 3 2 1
>>>

Generators

  • generator是用来生成一系列结果而非单一值的函数
def countdown(n):
   while n > 0:
       yield n
       n -= 1
>>> for i in countdown(5):
...     print i,
...
5 4 3 2 1
>>>
  • 该函数通过 yield 语句生成了一系列的值而非单一值
  • 与普通函数的行为迥异
  • 调用一个generator函数会创建一个 generator 对象。但是,它并未开始执行该函数。
def countdown(n):
   print "Counting down from", n
   while n > 0:
       yield n
       n -= 1
>>> x = countdown(10)   #注意调用该函数此时并未返回函数中print语句的内容
>>> x
<generator object at 0x58490>
>>>

Generator 函数

  • 函数在被调用 next() 时才执行
>>> x = countdown(10)
>>> x
<generator object at 0x58490>
>>> x.next()
Counting down from 10   #函数此时才开始执行
10
>>>

yield 生成的一个值,但立即暂停了函数的运行

  • 函数在下一次被调用 next() 时继续运行
>>> x.next()
9
>>> x.next()
8
>>>
  • generator 返回 StopIteration exception 时,枚举结束
>>> x.next()
1
>>> x.next()
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
StopIteration
>>>
  • generator函数的一大功用是能非常便利的编写获得一个 iterator
  • 你不必再去关心神马枚举协议(比如 .next, .__iter__, 诸如此类)
  • 轻松搞定

Generators 与 Iterators 的对比

  • generator 函数与支持枚举的对象略有不同
  • generator 只支持 一次性操作 。我们可以一次性的枚举遍历所有生成的数据。但如果希望再做一次,我们就必须再次调用这个generator函数。
  • 这与list就不一样了,我们可以任意次数的枚举遍历一个list

Generator 表达式

  • 基本上就是一个生成器版本的列表推导式(list comprehension)
>>> a = [1,2,3,4]
>>> b = (2*x for x in a)
>>> b
<generator object at 0x58760>
>>> for i in b: print b,
...
2 4 6 8
>>>
  • 该循环遍历一系列的对象并分别处理它们

  • 结果的值是通过generator一次一个生成出来的

  • 与列表推导式的重要不同是
    • 不会构建一个列表
    • 唯一用途是遍历枚举
    • 一旦被消费,无法重用
  • 举例:

>>> a = [1, 2, 3, 4]
>>> b = [2*x for x in a]
>>> b
[2, 4, 6, 8]
>>> c = (2*x for x in a)
<generator object at 0x58760>
>>>
  • 一般语法:

    (expression for i in s if cond1

    for j in t if cond2 ... if condfinal)

  • 意思是:

    for i in s:
    if cond1:
    for j in t:
    if cond2:

    ... if condfinal: yield expression

语法的注意事项

  • 如果将其用于一个函数的唯一参数,其括号可以不写
  • 比如:
sum(x*x for x in s)

小结

  • 我们目前有两种创建generator对象的方法
  • Generator函数:
def countdown(n):
    while n > 0:
        yield n
        n -= 1
  • Generator 表达式
squares = (x*x for x in s)
  • 用这两种方法我们都可以获得一个可以产生值的对象,一般我们会在for循环中“消费”它

第二部分:处理数据文件

编程任务

将Appache网络服务器的log文件中的最后一列的值 相加以计算总共传输了多少字节的数据。
81.107.39.38 - ... "GET /ply/ http/1.1" 200 7587
81.107.39.38 - ... "GET /favicon.ico htTP/1.1" 404 133
81.107.39.38 - ... "GET /ply/bookplug.gif HTTP/1.1" 200 2390
81.107.39.38 - ... "GET /ply/ply.html hTTP/1.1" 200 97238
81.107.39.38 - ... "GET /ply/example.html HTTP/1.1" 200 2359
66.249.72.134 - ... "GET /index.html httP/1.1" 200 4447

要注意的是这个log文件可能会很大,或许好几个Gbytes

Log 文件

  • 每一行的log看起来是这样的:

    81.107.39.38 - ... "GET /ply/bookplug.gif HTTP/1.1" 200 2390

  • 字节数显示在最后一列

bytestr = line.rsplit(None, 1)[1]
  • 它要么是一个数字要么是一个值缺失标志(-)

    81.107.39.38 - ... "GET /ply/ HTTP/1.1" 304 -

  • 类型转换

if bytestr != '-':
    bytes = int(bytestr)

一个非-Generator 的解决方案

  • 仅仅简单的使用一个for循环
wwwlog = open("access-log")
total = 0
for line in wwwlog:
    bytestr = line.rsplit(None, 1)[1]
    if bytestr != '-':
        total += int(bytestr)

print "Total", total
  • 我们一行一行的读取值并更新汇总数据
  • 可是,这个程序竟然运行了90秒钟...

一个Generator的解决方案

  • 我们会用到一些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)
  • 大不一样啊!
    • 更少的代码
    • 完全不一样的编程风格

Generators用做管道(Pipeline)

  • 要理解这个解决方案,我们可以把它想象成一个数据处理管道
              +--------+    +------------+    +-------+    +-------+
access-log -> | wwwlog | -> | bytecolumn | -> | bytes | -> | sum() | -> total
              +--------+    +------------+    +-------+    +-------+

未完待续...

Leave a Response