生成器是一个相对较新的Python概念,它是一种使用普通函数语法定义的迭代器。生成器和迭代器可能是今年来引入的最强大的功能,并且生成器是一个相当复杂的概念,要了解其工作原理需要花点时间。生成器能够让你编写出非常优雅的代码。
生成器创建和普通函数一样简单,下面通过一个简单的示例来说明生成器的用法:
创建一个将嵌套列表展开的函数,列表如下:
nested = [[1,2,3],[3],[4,5]]这是一个列表的列表,接下来需要将嵌套展开或按顺序提供这些数字,下面是一种生成器的解决方案:
def flatten(nested): for sublist in nested: for element in sublist: yield(element)这个函数代码很简单,它首先迭代所提供嵌套列表中的所有子列表,然后按顺序迭代每个子列表中的元素,如果将最后一行中的yield换成print就容易理解了。
注意,这里使用了关键字yield,包含yield语句的函数都被称为生成器。
这不仅仅是名称上的差别,生成器的行为与普通函数截然不同。差别在于,生成器不是使用return返回一个值,而是可以生成多个值,每次一个。每次使用yield生成一个值后,函数都将冻结,即在此停止执行,等待被重新唤起。被重新唤起后,函数将从停止的地方开始继续执行。
为使用所有的值,可对生成器进行迭代。
print(list(flatten(nested)))结果:
[1, 2, 3, 3, 4, 5]上面的示例智能处理两层的嵌套逻辑,使用两个for循环来实现的,如果要处理任意层嵌套的列表,这时候就不能用for循环来实现了,所以必须修改解决方案,可以使用递归式生成器。
使用递归式生成器重写以上函数:
def flatten(nested): try: for sublist in nested: for element in flatten(sublist): yield(element) except TypeError: yield nested调用flatten时,有两种可能性:基线条件和递归条件。在基线条件下,要求这个函数展开单个元素。这时候,for循环将引发TypeError异常,因为你视图迭代一个数,而这个生辰器只生成一个元素。
如果要展开的是一个列表(或其他任何可迭代对象),你就需要遍历所有子列表并对他们调用flatten,然后使用另一个for循环生成展开后的子列表中的所有元素。
print(list(flatten([[1,2,3],[1],[4,[5,6],7]])))输出:
[1, 2, 3, 1, 4, 5, 6, 7]但是,以上这个示例也存在一个问题,如果nested是字符串或类似字符串的对象,它就属于序列,因此不会引发TypeError异常,可你并不想对其进行迭代。
注意:在以上函数中,不应该对类似于字符串的对象进行迭代,主要原因有两个:
1.你想将类似于字符串的对象视为原子值,而不是应该展开的序列2.对这样的对象进行迭代会导致无穷递归,因为字符串的第一个元素是一个长度为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 print(list(flatten(['abc',[1,2],['ed','fg']])))输出:
['abc', 1, 2, 'ed', 'fg']如你所见,如果表达式nested+’'引发TypeError异常,就忽略这种异常;如果没有引发TypeError异常,内部try语句中的else子句将引发TypeError异常,这样讲在外部的excpet子句中原封不动地生成类似于字符串的对象。
生成器由两个单独的部分组成:生成器的函数和生成器的迭代器。生成器的函数是由def语句定义,其中包含yield。生成器的迭代器是这个函数的返回结果,这两个实体通常被视为一个,通称为生成器。
>>> def simple_generator(): ... yield 1 ... >>> simple_generator <function simple_generator at 0x104e4ab70> >>> simple_generator() <generator object simple_generator at 0x104d65a20> >>>对于生成器的函数返回的迭代器,可以像使用其他迭代器一样使用它。
生成器开始运行后,可以使用生成器和外部之间的通信渠道向它提供值,这个通信渠道包含如下两个端点:
外部世界:外部世界可访问生成器的方法send,这个方法类似于next,但接受一个参数(要发送的“消息”,可以是任何对象)生成器:在挂起的生成器内部,yield可能用作表达式而不是语句,也就是说,当生成器重新运行时,yield返回一个值----通过send从外部世界发送的值,如果使用的是next,yield将返回None。注意,仅当生成器被挂起(即遇到第一个yield)后,使用send(而不是next)才有意义。要在此之前向生成器提供信息,可使用生成器的函数的参数。 如果一定要在生成器刚启动时对其调用方法send,可向它传递参数None。
def repeater(value): while True: new = (yield value) if new is not None:value = new r = repeater(12) print(next(r)) print(r.send(34))输出
12 34生成器的另外两个方法:
方法throw:用于在生成器中(yield表达式处)引发异常,调用时可提供一个异常类型、一个可选值和一个traceback对象方法close:用于停止生成器,调用时无需提供任何参数。方法close(由Python垃圾收集器在需要时调用)也是基于异常的:在yield处引发GeneratorExit异常。因此如果要在生成器中提供一些清理代码,可将yield放在一条try/finally语句中。如果愿意,也可以捕获GeneratorExit异常,但随后必须重新引发它(可能在清理后)、引发其他异常或直接返回。对生成器调用close后,再试图从它那里获取值将导致RuntimeError异常。
前面我们说到,生成器可以让你能够写出非常优雅的代码,但是,无论编写什么程序,都完全可以不使用生成器。并且在一些老版本的Python没有生成器的概念,那么,要模拟这种用法该怎么办呢。
下面提供了一个简单的解决方案,让你能够使用普通函数模拟生成器。
首先在函数的开头插入如下一行代码: result=[] 然后,将类似于yield some_expression的代码行替换如下代码行:
result.append(some_expression)最后,在函数末尾添加: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): # yield(element) result.append(element) except TypeError: # yield nested result.append(nested) return result print(list(flatten([[1,2,3],[1],[4,[5,6],7]]))) print(list(flatten(['abc',[1,2],['ed','fg']])))输出:
[1, 2, 3, 1, 4, 5, 6, 7] ['abc', 1, 2, 'ed', 'fg']