欲先了解生成器,必先了解迭代器。迭代器参考:

Python函数式编程指南(三):迭代器


Python函数式编程指南(四):生成器


作者的《Python函数式编程指南:目录和参考》系列文章写的很好,推荐阅读:


 

以下大部分整理摘自:《Python 基础教程》    9.7 生成器    p153

创建生成器

生成器是Python新引入的概念,由于历史原因,它也叫简单生成器。它和迭代器可能是近几年来引入的最强大的两个特性。但是,生成器的概念则要更高级一些,需要花些工夫才能理解它是如何工作的以及它有什么用处。生成器可以帮助读者写出非常优雅的代码,当然,编写任何程序时不使用生成器也是可以的。

生成器是一种用普通的函数语法定义的迭代器。    ——这句话揭露了生成器的本质。


下面用一个例子来说明生成器的知识。首先创建一个展开嵌套列表的函数。参数是一个列表,和下面这个很想:

nested = [[1, 2], [3, 4], [5]]

换句话说就是一个列表的列表。函数应该按顺序打印出列表中的数字。解决的办法如下:

def flatten(nested):
    for sublist in nested:
        for element in sublist:
            yield element

nested = [[1, 2], [3, 4], [5]]
for num in flatten(nested):
    print num
print list(flatten(nested))

 

下面这一段摘自:

《Python 参考手册》    1.12 生成器    p14    有几个很好的使用例子

《Python 参考手册》    6.6 生成器与yield    p83

 

任何使用yield语句的函数都称为生成器。调用生成器函数将创建一个对象,该对象通过连续调用next()方法(在python3中是__next__())生成结果序列。

next()调用使生成器函数一直运行到下一条yield语句为止。此时next()将返回值传递给yield,而且函数将暂时中止执行。再次调用next()时,函数将继续执行yield之后的语句。此过程持续到函数返回为止。

通常不会在生成器上直接调用next()方法,而是在for语句、sum()或一些使用序列的其他操作中使用它。

生成器函数完成的标志是返回或引发StopIteration异常,这标志着迭代的结束。如果生成器在完成时返回None之外的值,都是不合法的。

 

生成器是由两部分组成:生成器的函数生成器的迭代器

生成器的函数是用def 语句定义的,包含yield的部分;生成器的迭代器是这个函数返回的部分。按一种不是很准确的说法,两个实体经常被当作一个,合起来叫做生成器。

>>> def simple_generator():
...     yield 1
... 
>>> simple_generator
<function simple_generator at 0x16eb398>
>>> simple_generator()
<generator object simple_generator at 0x16cfc30>
>>>

生成器的函数返回的迭代器可以像其他迭代器那样使用。

 

递归生成器

上面创建的生成器只能处理两层嵌套,如果要处理任意层的嵌套该怎么办?例如,可能要使用来表示树结构。每层嵌套需要增加一个for循环,但因为不知道有几层嵌套,所以必须把解决方案变得更灵活。现在是求助于递归(recursion)的时候了。

def flatten(nested):
    try:
        for sublist in nested:
            for element in flatten(sublist):
                yield element
    except TypeError:
        yield nested

nested = [0, [1, 2], [3, 4], [5, [6, 7], 8]]
print list(flatten(nested))

当flatten被调用时,有两种可能性(处理递归时大部分时都是有两种情况):基本情况和需要递归的情况。在基本的情况中,函数被告知展开一个元素(比如一个数字),这种情况下,for循环会引发一个TypeError异常(因为试图对一个数字进行迭代),生成器会产生一个元素。

TypeError: 'int' object is not iterable

如果展开的是一个列表(或者其他的可迭代对象),那么就要进行特殊处理。程序必须遍历所有的子列表(一些可能不是列表),并对它们调用flatten。然后使用另一个for循环来产生被展开的子列表的所有元素。这可能看起来有点不可思议,但却能工作。

这么做有一个问题:如果nested是一个类似字符串的对象,那么它就是一个序列,不会引发TypeError,但是你不想对这样的对象进行迭代。

注意:不应该在flatten函数中对类似于字符串的对象进行迭代,出于两个主要的原因。首先,需要实现的是将类似于字符串的对象当成原子值,而不是当成应被展开的序列。其次,对它们进行迭代实际上会导致无穷递归,因为一个字符串的第一个元素是另一个长度为1的字符串,而长度为1的字符串的第一个元素就是字符串本身。

为了处理这种情况,则必须在生成器的开始处添加一个检查语句。试着将传入的对象和一个字符串拼接,看看会不会出现TypeError,这是检查一个对象是不是类于字符串的最简单、最快速的方法。

def flatten(nested):
    try:
        try:
            nested + ''
        except TypeError:
            pass
        else:
            raise TypeError

        for sublist in nested:
            for element in flatten(sublist):
                yield element
    except TypeError:
        yield nested

nested = [0, [1, 2], [3, 4], [5, [6, 7], 8]]
print list(flatten(nested))

 

生成器方法

不再使用或删除生成器时,就会调用close()方法。通常不必手动调用,但也可以这样做。在生成器函数内部,在yield语句上出现GeneratorExit异常时就会调用close()方法。可以选择捕捉这个异常,以便执行清理操作。虽然可以捕捉GeneratorExit异常,但生成器函数处理异常并使用yield语句生成另一个输出值是不合法的。另外,如果程序当前正在通过生成器进行迭代,决不能通过单独的执行线程或信号处理程序在该生成器上异步地调用close()方法。

 

模拟生成器

生成器在旧版本的Python中是不可用的。下面介绍的就是如何使用普通的函数模拟生成器。(是不用生成器的实现方式,不是实现一个生成器)

0、递归的实现的方式,要没有用到静态变量(默认参数为列表的变量)或者全局变量!

原因及这个例子的详细分析见:


1、先从生成器的代码开始。首先将下面语句放在函数体的开始处:

result = []


2、然后将下面这种形式的代码:

yield some_expression

用下面的语句替换:

result.append(some_expression)


3、最后,在函数的末尾,添加下面这条语句:

return result

尽管这个版本可能不适用于所有生成器,但对大多数生成器来说是可行的(比如,它不能用于一个无限的生成器,当然不能把它的值放入列表中)。

下面是flatten生成器用普通的函数重写的版本:

def flatten(nested):
    result = []
    try:
        try:
            nested + ''
        except TypeError:
            pass
        else:
            raise TypeError

        for sublist in nested:
            for element in flatten(sublist):
                result.append(element)
    except TypeError:
        result.append(nested)
    return result

nested = [0, [1, 2], [3, 4], [5, [6, 7], 8]]
print flatten(nested)

反过来,如果发现程序写成这种形式,可以升级为生成器实现。