生成器是一个相对较新的Python概念。 由于历史原因,它也被称为简单生成器(simple generator)。 生成器和迭代器可能是近年来引入的最强大的功能, 但生成器是一个相当复杂的概念,可能需要花些功夫才能明白其工作原理和用途。
虽然生成器能够编写出非常优雅的代 码,但请放心,无论编写什么程序,都完全可以不使用生成器。
生成器是一种使用普通函数语法定义的迭代器。生成器的工作原理到底是什么呢? 通过示例 来说明最合适。下面先来看看如何创建和使用生成器,然后再看看幕后的情况。
创建一个将嵌套列表展开的函数。这个函数将一个类似于下面的列表作为参数:
nested = [[1, 2], [3, 4], [5]]
换而言之,这是一个列表的列表。函数应按顺序提供这些数字,下面是一种解决方案:
def flatten(nested):
for sublist in nested:
for element in sublist:
yield element
这个函数的大部分代码都很简单。它首先迭代所提供嵌套列表中的所有子列表,然后按顺序 迭代每个子列表的元素。
倘若最后一行为 print(element)
,这个函数将容易理解得多,不是吗?
在这里,没有 yield
语句。包含 yield
语句的函数都被称为生成器。
这可不仅仅是名称上的差别,生成器的行为与普通函数截然不同。
差别在于,生成器不是使用 return
返回一个值,而是可以生成多个值,每次一个。
每次使用 yield
生成一个值后,函数都将冻结,即在此停止执行,等待被重新唤醒。
被重新唤醒后,函数将从停止的地方开始继续执行。
iter_obj = flatten(nested)
print(iter_obj)
<generator object flatten at 0x7fef7543c930>
为使用所有的值,可对生成器进行迭代。
# nested = [[1, 2], [3, 4], [5]]
for num in iter_obj:
print(num)
1 2 3 4 5
生成器可以通过 list()
函数转换成列表。但是要注意一下问题,如下:
list(iter_obj)
[]
上面返回的结果为空。 尽管也是用 for
进行遍历,但是生成器与列表结构有明显的差异。
生成器在遍历后程序会记住其遍历过的状态。如果已经遍历完,则剩余的结果为空。
如果要得到完整的结果,得重新生成生态器对象:
iter_obj = flatten(nested)
next(iter_obj)
1
next(iter_obj)
2
list(iter_obj)
[3, 4, 5]
在Python 2.4中,引入了一个类似于列表推导的概念: 生成器推导 (也叫生成器表达式)。 其工作原理与列表推导相同,但不是创建一个列表(即不立即执行循环),而是返回一个生成器,能够逐步执行计算。
g = ((i + 2) ** 2 for i in range(2, 27))
g
<generator object <genexpr> at 0x7fef7543f100>
next(g)
16
不同于列表推导,这里使用的是圆括号。
在像这样的简单情形下,使用列表推导更直接; 但如果要包装可迭代对象(可能生成大量的值), 使用列表推导将立即实例化一个列表,从而丧失迭代的优势。
另一个好处是,直接在一对既有的圆括号内(如在函数调用中)使用生成器推导时, 无需再添加一对圆括号。换而言之,可编写下面这样非常漂亮的代码:
sum(i ** 2 for i in range(10))
285
前一节设计的生成器只能处理两层的嵌套列表,这是使用两个 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
循环生成展开后的子列表中的所有元素。
这可能看起来有点不可思议,但确实可行。
list(flatten([[[1], 2], 3, 4, [5, [6, 7]], 8]))
[1, 2, 3, 4, 5, 6, 7, 8]
然而,这个解决方案存在一个问题。如果 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 + ''
引发了 TypeError
异常,就忽略这种异常;
如果没有引发 TypeError
异常,内部 try
语句中的else子句将引发 TypeError
异常,
这样将在外部的excpet子句中 原封不动地生成类似于字符串的对象。明白了吗?
下面的示例表明,这个版本也可用于字符串:
list(flatten(['foo', ['bar', ['baz']]]))
['foo', 'bar', 'baz']
请注意,这里没有执行类型检查:没有检查 nested
是否是字符串,而只是检查其行为是否类似于字符串,
即能否与字符串拼接。对于这种检查,一种更自然的替代方案是,
使用 isinstance
以及字符串和类似于字符串的对象的一些抽象超类,但遗憾的是没有这样的标准类。
另外,即便 是对 UserString
来说,也无法检查其类型是否为 str
。
如果按前面的例子做了,就差不多知道了如何使用生成器。
生成器是包含关键字 yield
的函数,但被调用时不会执行函数体内的代码,
而是返回一个迭代器。每次请求值时,都 将执行生成器的代码,直到遇到 yield
或 return
。
yield
意味着应生成一个值,而 return
意味着生成器应停止执行
(即不再生成值;仅当在生成器调用 return
时,才能不提供任何参数)。
换而言之,生成器由两个单独的部分组成:生成器的函数和生成器的迭代器。
生成器的函数是由 def
语句定义的,其中包含 yield
。
生成器的迭代器是这个函数返回的结果。用不太准确的话说,这两个实体通常被视为一个,通称为生成器。
def simple_generator():
yield 1
simple_generator
<function __main__.simple_generator()>
simple_generator()
<generator object simple_generator at 0x7fef75471220>
对于生成器的函数返回的迭代器,可以像使用其他迭代器一样使用它。
在生成器开始运行后,可使用生成器和外部之间的通信渠道向它提供值。这个通信渠道包含如下两个端点。
- 外部世界:外部世界可访问生成器的方法
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(42)
next(r)
42
r.send("Hello, world!")
'Hello, world!'
注意到使用圆括号将 yield
表达式括起来了。在有些情况下,并非必须这样做,
但小心驶得万年船。如果要以某种方式使用返回值,就不管三七二十一,将其用圆括号括起吧。
生成器还包含另外两个方法。
- 方法
throw()
:用于在生成器中(yield
表达式处)引发异常, 调用时可提供一个异常类型、一个可选值和一个traceback
对象。 - 方法
close()
:用于停止生成器,调用时无需提供任何参数。
方法 close()
(由 Python 垃圾收集器在需要时调用) 也是基于异常的: 在 yield
处引发 GeneratorExit
异常。
因此如果要在生成器中提供一些清理代码,可将 yield
放在一条 try/finally
语句中。
如果愿意,也可捕获 GeneratorExit
异常,但随后必须重新引发它(可能在清理后)、引 发其他异常或直接返回。
对生成器调用 close()
后,再试图从它那里获取值将导致 RuntimeError
异常。
提示 有关生成器的方法以及它们是如何将生成器变成简单协同程序(coroutine)的详细信息, 请参阅“PEP 342”(www.python.org/dev/peps/pep-0342/ )。
如果使用的是较老的Python版本,就无法使用生成器。 下面是一个简单的解决方案,使用普通函数模拟生成器。
首先,在函数体开头插入如下一行代码:
result = []
如果代码已使用名称 result
,应改用其他名称。
(在任何情况下,使用更具描述性的名称都是不错的主意。)
接下来,将类似于 yield some_expression
的代码行替换为如下代码行:
yield some_expression with this:
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):
result.append(element)
except TypeError:
result.append(nested)
return result