>>> from env_helper import info; info()
页面更新时间: 2023-06-22 23:05:13
运行环境:
Linux发行版本: Debian GNU/Linux 12 (bookworm)
操作系统内核: Linux-6.1.0-9-amd64-x86_64-with-glibc2.36
Python版本: 3.11.2
11.3. 生成器¶
生成器是一个相对较新的Python概念。 由于历史原因,它也被称为简单生成器(simple generator)。 生成器和迭代器可能是近年来引入的最强大的功能, 但生成器是一个相当复杂的概 念,你可能需要花些功夫才能明白其工作原理和用途。 虽然生成器让你能够编写出非常优雅的代 码,但请放心, 无论编写什么程序,都完全可以不使用生成器。
生成器是一种使用普通函数语法定义的迭代器。生成器的工作原理到底是什么呢? 通过示例 来说明最合适。下面先来看看如何创建和使用生成器,然后再看看幕后的情况。
11.3.1. 创建生成器¶
生成器创建起来与函数一样简单。你现在肯定厌烦了老套的斐波那契数列,所以下面换换口 味,创建一个将嵌套列表展开的函数。这个函数将一个类似于下面的列表作为参数:
>>> 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 0x7fc257c6cba0>
为使用所有的值,可对生成器进行迭代。
>>> nested = [[1, 2], [3, 4], [5]]
>>> for num in flatten(nested):
>>> print(num)
1
2
3
4
5
或
>>> list(flatten(nested))
[1, 2, 3, 4, 5]
11.3.2. 简单生成器¶
在Python 2.4中,引入了一个类似于列表推导的概念: 生成器推导 (也叫生 成器表达式)。 其工作原理与列表推导相同,但不是创建一个列表(即不立即执行循环),而 是返回一个生成器,让你能够逐步执行计算。
>>> g = ((i + 2) ** 2 for i in range(2, 27))
>>> g
<generator object <genexpr> at 0x7f6bcaafa190>
>>> next(g)
16
如你所见,不同于列表推导,这里使用的是圆括号。在像这样的简单情形下,还不如使 用列表推导;但如果要包装可迭代对象(可能生成大量的值),使用列表推导将立即实例化一 个列表,从而丧失迭代的优势。
另一个好处是,直接在一对既有的圆括号内(如在函数调用中)使用生成器推导时,无需 再添加一对圆括号。换而言之,可编写下面这样非常漂亮的代码:
>>> sum(i ** 2 for i in range(10))
285
11.3.3. 递归式生成器¶
前一节设计的生成器只能处理两层的嵌套列表,这是使用两个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。
11.3.4. 通用生成器¶
如果你按前面的例子做了,就差不多知道了如何使用生成器。你知道,生成器是包含关键字 yield的函数,但被调用时不会执行函数体内的代码,而是返回一个迭代器。每次请求值时,都 将执行生成器的代码,直到遇到yield或return。yield意味着应生成一个值,而return意味着生 成器应停止执行(即不再生成值;仅当在生成器调用return时,才能不提供任何参数)。
换而言之,生成器由两个单独的部分组成:生成器的函数和生成器的迭代器。生成器的函数 是由def语句定义的,其中包含yield。生成器的迭代器是这个函数返回的结果。用不太准确的话 说,这两个实体通常被视为一个,通称为生成器。
>>> def simple_generator():
yield 1
...
>>> simple_generator
<function simple_generator at 153b44>
>>> simple_generator()
<generator object at 1510b0>
<generator object at 1510b0>
对于生成器的函数返回的迭代器,可以像使用其他迭代器一样使用它。
11.3.5. 生成器的方法¶
在生成器开始运行后,可使用生成器和外部之间的通信渠道向它提供值。这个通信渠道包含 如下两个端点。
外部世界:外部世界可访问生成器的方法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/ )。
11.3.6. 模拟生成器¶
如果你使用的是较老的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