概述
生成器是一个相对较新的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']