Python 产生器与迭代器

1,李木头的Python学习]Iterator 和 Generator的学习心得

    把递归处理变成线性处理

Iterator是迭代器的意思,它的作用是一次产生一个数据项,直到没有为止。这样在 for 循环中就可以对它进行循环处理了。那么它与一般的序列类型(list, tuple等)有什么区别呢?它一次只返回一个数据项,占用更少的内存。但它需要记住当前的状态,以便返回下一数据项。它是一个有着next()方法的对象。而序列类型则保存了所有的数据项,它们的访问是通过索引进行的。

使用Iterator的好处除了节省内存外,还有一个好处就是可以把非线性化的处理转换成线性化的方式来进行处理。如对一棵树的访问,传统的方法可以使用递归函数来处理,下面是对树的一个中序遍历的示例:

例1:

def deal_tree(node):
    if not node:
        return
    if node.leftnode:
        deal_tree(node.leftnode)
    process(node)
    if node.rightnode:
        deal_tree(node.rightnode)

deal_tree(root)

可以看出,对结点的处理函数与递归函数是混在一起的,不是很清晰。使用Iterator的方式改写后为:

例2:

1    def walk_tree(node):
2        if not node:
3            return
4        if node.leftnode:
5            for i in walk_tree(node.leftnode):
6                yield i
7        yield node
8        if node.rightnode:
9            for i in walk_tree(node.rightnode):
10               yield i
11
12   for node in wald_tree(root):
13       process(node)

生成结点的过程仍然是一个递归过程,但对于返回后的结点的处理就变成了线性化的处理,结构上要清晰多了。第5-6,9-10行要特别注意,如果不这样处理直接调用walk_tree的话,其实返回的是一个Iterator对象,而不是想要的元素。

象上面的walk_tree函数在 python中可以叫作Generator--产生器,它的作用是生成一个Iterator的对象。那么它主要是将一个函数过程进行封装,转化为Iterator对象,每执行到yield语句时,函数的状态,数据都保存起来,然后返回相应的值。取下一个值的时候,再从上次运行的地方继续运行,如果遇上yield语句,则再次保存状态,返回结果,如果不存在值了,则自动引发一个异常StopIteration,从而Iterator不再产生新的值。从此处我们可以了解,这里的Iterator只可以遍历一次,但并非所有的都是这样,你完全可以对其进行控制。

下面我再介绍一下如何构造自已的Iterator。很简单,创建一个类,满足Iterator的协议,也就是要定义__iter__方法,它返回一个Iterator对象,这个对象必须有next方法,因此我们可以总结出两种对象模式:

class A:
    def __iter__(self):
        return self

    def next(self):
        if has_next_value(self):
            return next_value
        else:
            raise StopIteration

class B:
    def __iter__(self):
        return iterator_obj

A,B分别为两种对象模式(都是示例代码)。模式A表示,在A中定义了next方法,因此__iter__简单地返回自身即可。当不存在下一个值时,引发StopIteration异常。模式B表示,它使用了其它的Iterator对象,因此只需要定义__iter__即可,next不需要定义,因为返回的Iterator对象已经含有next方法了。如果是自已实现next方法,那么在返回值之前需要记住当前的状态,以便下一次运行时,可以取下一个值。

第2个例子好象与这里讲的不一样啊。这就是前面讲的Generator,它的作用就是把一个函数转换成一个Iterator,它自动保存状态,中间数据,引发异常,全部是自动化了。而且它只可以遍历一次。如果想再次遍历,只有重新生成新的Iterator对象才可以。

在最新的 python2.4 版中新增了Genetaor Expression方式,它是用来生成简单的,在函数调用需要序列参数时的一种Iterator写法,语法就象是list comprehension的格式,如:

>>> sum(i*i for i in range(10))                 # sum of squares
285

不过这种写法必须要在小括号对中,因此它的使用是有限的。它的目的主要是想更好的使用内存。

前面我们提到不是所有的Iterator只可以遍历一次(使用Generator生成的只能遍历一次),你完全可以控制它重新遍历。比如我们可以在Iterator对象中增加一个复位方法,用来将内部的计数恢复到开始状态,这样我们就可以重新遍历了。

下面我们总结一下:

Iterator对象:具有__iter__方法,和next方法。当没有新值时引发StopIteration异常。

Iterator的好处:在某些情况下可以使程序结构清晰,如将递归等非线性处理转为线性处理。可以减少内存的占用。

Generator:将一个函数转化成Iterator对象的方法。使用它只需要在函数中需要返回值的时候调用yield语句。它是生成Iterator对象的简单方法,只适用于函数。

2 深入了解python暂缓列表生成器

有的时候你的程序精力过剩,把你不需要或者不希望它做的事情都给做了——你想要它懒一点才好。这就是生成器的用武之地。使用python生成器(generator)能够让你精确地决定要它做多少以及什么时候去做。

上周我们向你介绍了python列表推导(list comprehension),它让你能以更加自然的方式来表示列表的内容。本文将介绍它们的同类:python生成器,它可以一段一段地构成一个序列,按照你的要求完成工作量。

这被叫做暂缓求值(lazy evaluation),用来推迟某个特定值的计算,直到程序某个点需要才开始。暂缓求值在很多类型的编程中非常有用,因为它不用计算从来都不使用的值,因而可以带来性能上的提升。有的语言,比如Haskell和Miranda,在默认情况下都使用暂缓求值;而其他的语言,像Scheme、Ocaml和python,都可以通过专门的句法来使用暂缓求值。在python里,实现暂缓求值的方式是使用生成器

利用生成器你可以编写出这样一个函数,它能够返回结果然后暂停,当你下一次调用这个函数的时候它会从暂停的地方恢复。你不需要任何专门的句法来编写生成器,要做的只是表示一下什么时候使用yield语句返回一个中间值。说明这一点的最简单方法是举一个例子:

>>> def generator1():
...     yield 'first'
...     yield 'second'
...     yield 'third'
...
>>> gen=generator1()
>>> gen
<generator object generator1 at 0x013511C0>
>>> gen.next()
'first'
>>> gen.next()
'second'
>>> gen.next()
'third'
>>> gen.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

在这个例子中,我们首先定义一个叫做generator1的生成器,它会生成三个值:“first”、“second”和“third”三个字符串。当我们创建一个新的生成器对象(gen)时,它就开始执行函数。每当你调用生成器next方法时,它会继续执行这个函数,直到生成下一个值。当生成器达到块结尾的时候,或者碰到返回语句时,它会产生一个StopIteration异常。

生成器本质上就是迭代器,这也就是说它们可以用在很多表达式里,而不需要用序列(列表等)。例如,你可以不使用上面的代码,而改用下面的:

>>> gen2=generator1()
>>> for i in gen2:
...     print i
...
first
second
third

类似的,你可以一次就把生成器转换成为列表:

>>> gen3=generator1()
>>> list(gen3)
['first', 'second', 'third']

下面就让我们举一个真正派得上用场的例子。下面是一个相当标准的用于产生组合的函数,换句话说,一个序列可以以多种独特的方式被切割成小的序列:

def combination(seq, length):
    if not length:
        return [[]]
    else:
        l = []
        for i in xrange(len(seq)):
            for result in combination(seq[i+1:], length-1):
                l+= [[seq[i]]+result]
        return l
           
print combination("abcde",3)
print combination('ABCDEF',4)

结果:

[['a', 'b', 'c'], ['a', 'b', 'd'], ['a', 'b', 'e'], ['a', 'c', 'd'], ['a', 'c', 'e'], ['a', 'd', 'e'], ['b', 'c', 'd'], ['b', 'c', 'e'], ['b', 'd', 'e'], ['c', 'd', 'e']]
[['A', 'B', 'C', 'D'], ['A', 'B', 'C', 'E'], ['A', 'B', 'C', 'F'], ['A', 'B', 'D', 'E'], ['A', 'B', 'D', 'F'], ['A', 'B', 'E', 'F'], ['A', 'C', 'D', 'E'], ['A', 'C', 'D', 'F'], ['A', 'C', 'E', 'F'], ['A', 'D', 'E', 'F'], ['B', 'C', 'D', 'E'], ['B', 'C', 'D', 'F'], ['B', 'C', 'E', 'F'], ['B', 'D', 'E', 'F'], ['C', 'D', 'E', 'F']]

 现在让我们改用生成器来做同样的事情。你想要在某些点往最终结果列表里加入一个值并用yield语句替换掉它,因此这里的技巧是简单地替换掉每一个这样的点

 实例:

def xcombination(seq,length):
    if not length:
        yield []
    else:
        for i in xrange(len(seq)):
            for result in xcombination(seq[i+1:], length-1):
                yield [seq[i]]+result
               
comb=xcombination('ABCDEF',3)
print comb.next()
print comb.next()
print comb.next()
print "-----------------"
print list(comb)
print "=================="
comb2=xcombination("ABCDE",2)
for i in xrange(3):
    print comb2.next()

结果:

['A', 'B', 'C']
['A', 'B', 'D']
['A', 'B', 'E']
-----------------
[['A', 'B', 'F'], ['A', 'C', 'D'], ['A', 'C', 'E'], ['A', 'C', 'F'], ['A', 'D', 'E'], ['A', 'D', 'F'], ['A', 'E', 'F'], ['B', 'C', 'D'], ['B', 'C', 'E'], ['B', 'C', 'F'], ['B', 'D', 'E'], ['B', 'D', 'F'], ['B', 'E', 'F'], ['C', 'D', 'E'], ['C', 'D', 'F'], ['C', 'E', 'F'], ['D', 'E', 'F']]
==================
['A', 'B']
['A', 'C']
['A', 'D']
在最后一条命令里,尽管有10种不同的字母组合,但是只生成了3个,与标准的函数相比,这就节省了70%的计算时间。

 生成器真正的有用之处在于,在大多数情况下它们可以用作替代列表推导的放下(Drop)。你需要做的是用圆括号替换掉列表推导前后的方括号。我们就举《列表推导》一文中的最后一个例子。我们不需要:

>>> guests=['chirs','brendam','jimmy','mel','mike','jess']
>>> [(seat1,seat2) for seat1 in guests for seat2 in guests if seat1!=seat2]

结果:
[('chirs', 'brendam'), ('chirs', 'jimmy'), ('chirs', 'mel'), ('chirs', 'mike'),
('chirs', 'jess'), ('brendam', 'chirs'), ('brendam', 'jimmy'), ('brendam', 'mel'
), ('brendam', 'mike'), ('brendam', 'jess'), ('jimmy', 'chirs'), ('jimmy', 'bren
dam'), ('jimmy', 'mel'), ('jimmy', 'mike'), ('jimmy', 'jess'), ('mel', 'chirs'),
 ('mel', 'brendam'), ('mel', 'jimmy'), ('mel', 'mike'), ('mel', 'jess'), ('mike'
, 'chirs'), ('mike', 'brendam'), ('mike', 'jimmy'), ('mike', 'mel'), ('mike', 'j
ess'), ('jess', 'chirs'), ('jess', 'brendam'), ('jess', 'jimmy'), ('jess', 'mel'
), ('jess', 'mike')]

可以改为:

>>> guests=['chirs','brendam','jimmy','mel','mike','jess']

>>> ((seat1,seat2) for seat1 in guests for seat2 in guests if seat1!=seat2)

 >>> seating=((seat1,seat2) for seat1 in guests for seat2 in guests if seat1!=sea
t2)
>>> for i in xrange(10):
...     print seating.next()
...
('chirs', 'brendam')
('chirs', 'jimmy')
('chirs', 'mel')
('chirs', 'mike')
('chirs', 'jess')
('brendam', 'chirs')
('brendam', 'jimmy')
('brendam', 'mel')
('brendam', 'mike')
('brendam', 'jess')

到现在我们已经在使用生成器了,在创建列表上它比其他方法节约计算时间。这非常好,但是它大展拳脚的地方是在不可能计算整个列表的时候。我们就以Fibonacci序列为例,在这个序列里,每个数字都是前面两个数字的和。假设我们想要一个能够生成到指定数字的序列:

实例:

>>> def fib(n):
...     a,b=0,1
...     l=[1]
...     while b<n:
...             l+=[b]
...             a,b=b,a+b
...     return l
...
>>> fib(20)
[1, 1, 1, 2, 3, 5, 8, 13]

现在它运行良好,除非我们想要它停止,否则它会一直计算到我们指定的数字。但是,如果我们想要更改停止的条件,我们就必须重写这个函数。但是我们可以用生成器来实现它(从python的《python.org/dev/peps/pep-0255/">PEP生成器》一文借用的实现):

>>> def xfib():
...     a,b=0,1
...     while True:
...             yield b
...             a,b=b,a+b
...
>>> fibseries=xfib()
>>> b=fibseries.next()
>>> while b<20:
...     print b
...     b=fibseries.next()
...
1
1
2
3
5
8
13

或者,如果我们想要在第一个回文(超过一位)处停止,我们只需要改变循环条件就行了:

>>> fibseries=xfib()
>>> b=fibseries.next()
>>> while b<10 or not list(str(b))==list(reversed(str(b))):
...     print b
...     b=fibseries.next()
...
1
1
2
3
5
8
13
21
34 

这就行了(这个数列的下一个值是55)。但是我们通过使用生成器可以让列表生成实现与什么时候停止生成它的逻辑分离,同时只用计算我们需要的那么多值。

你应该在什么时候使用生成器而不用列表推导呢?首先,如果你准备使用完整的列表,你就最好使用列表推导——它们的速度会更快一些,因为不会有调用生成器函数所增加的系统开销。如果你准备使用列表的第一部分,那就使用生成器吧,因为这会节约你的CPU时间。

原文地址:https://www.cnblogs.com/jerryxing/p/2825866.html