>>> 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表达式括起来了。在有些情况下,并非必须这样做,但小心驶得 万年船。如果要以某种方式使用返回值,就不管三七二十一,将其用圆括号括起吧。

生成器还包含另外两个方法。

  1. 方法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