第 十七 章迭代器、生成器和经典协程

当我在自己的程序中看到模式时,我认为这是一个麻烦的迹象。程序的形状应该只反映它需要解决的问题。代码中的任何其他规律性都表明,至少对我来说,我对抽象的理解还不够深——通常是我手动完成的事情,本应该通过写代码来让宏的扩展自动实现。

 

                                                --Paul Graham, Lisp hacker and venture capitalist

迭代是数据处理的基础:不管数据是像素还是核苷酸,程序将计算应用于数据集。如果数据在内存中放不下,我们需要惰性获取数据线——一次一个,按需获取。这就是迭代器所做的。本章展示了迭代器设计模式是如何内置到 Python 语言中的,因此您无需手动编写代码实现。

Python 中的每个标准集合都是可迭代的。可迭代对象是提供迭代器的对象,Python 使用它来支持以下操作:

  • for 循环
  • 列表、字典和集合推导式;
  • 解包赋值;
  • 集合实例的构建。

本章涵盖以下主题:

  • Python 如何使用 iter() 内置函数来处理可迭代对象;
  • 如何在 Python 中实现经典的迭代器模式;
  • 如何用生成器函数或生成器表达式替换经典的 Iterator;
  • 生成器函数的详细工作原理,逐行描述;
  • 利用标准库中的通用生成器函数;
  • 使用 yield from 表达式合并生成器;
  • 为什么生成器和经典协程看起来很相似,但使用方式却截然不同,不能混淆。

本章的新内容

Subgenerators with yield from”从 1 页增加到 6 页。它现在包括更简单的实验,演示生成器与 yield from 的行为,以及逐步开发的遍历树数据结构的示例。

新部分解释了 Iterable、Iterator 和 Generator 类型的类型提示。

本章的最后一个主要部分“经典协程”是对一个主题的 9 页介绍,该主题占了第一版 40 页的章节。我更新了经典协程章节并将其移到了配套网站的一篇文章中,因为它对读者来说是最具挑战性的章节,但是在 Python 3.5 引入了原生协程之后,这个的主题变得不那么重要了——我们将在第 21 章中学习。

我们将开始研究 iter() 内置函数如何使序列可迭代。

单词序列

我们将通过实现一个 Sentence 类来开始我们对迭代的探索:构造函数传入一个包含文本的字符串,然后你可以逐词迭代。第一个版本将实现序列协议,并且它是可迭代的,因为所有序列都是可迭代的——正如我们从第 1 章开始看到的那样。现在我们将说明真正的原因。

示例 17-1 展示了一个 Sentence 类,它通过索引从文本中提取单词。

例 17-1。 sentence.py:作为单词序列的Sentence类

import re
import reprlib

RE_WORD = re.compile(r'\w+')


class Sentence:

    def __init__(self, text):
        self.text = text
        self.words = RE_WORD.findall(text)  1

    def __getitem__(self, index):
        return self.words[index]  2

    def __len__(self):  3
        return len(self.words)

    def __repr__(self):
        return 'Sentence(%s)' % reprlib.repr(self.text)  4
  1. findall 返回一个包含正则表达式的所有非重叠匹配的字符串列表。
  2. self.words 保存了 .findall 的结果,所以我们直接返回给定索引处的单词。
  3. 为了完成序列协议,我们实现了`len`,尽管不需要这个方法就可以让对象可迭代。
  4. reprlib.repr 是一个实用函数,用于生成可能非常大的数据结构的缩写字符串表示。

默认情况下,reprlib.repr 将生成的字符串限制为 30 个字符。请参阅示例 17-2 中的控制台会话以了解 Sentence 的使用方式。

例 17-2。在 Sentence 实例上测试迭代

>>> s = Sentence('"The time has come," the Walrus said,')  1
>>> s
Sentence('"The time ha... Walrus said,')  2
>>> for word in s:  3
...     print(word)
The
time
has
come
the
Walrus
said
>>> list(s)  4
['The', 'time', 'has', 'come', 'the', 'Walrus', 'said']
  1. 从一个字符串中创建一个Sentence对象
  2. 注意 __repr__ 的输出包含 由 reprlib.repr 生成的... 。
  3. Sentence实例是可迭代的;我们马上就会明白为什么。
  4. 由于是可迭代的,Sentence 对象可用于构建列表和其他可迭代类型。

在接下来的页面中,我们将开发通过示例 17-2 中的测试的其他 Sentence 类。但是,示例 17-1 中的实现与其他实现不同,因为这一版它也是一个序列,因此您可以通过索引获取单词:

>>> s[0]
'The'
>>> s[5]
'Walrus'
>>> s[-1]
'said'

Python 程序员知道序列是可迭代的。现在我们将了解具体的原因。

为什么序列是可迭代的:iter 函数

每当 Python 需要迭代对象 x 时,它都会自动调用 iter(x)。

iter 内置函数:

  1. 检查对象是否实现了 __iter__,并调用它来获取一个迭代器。
  2. 如果未实现 __iter__ ,但实现了 __getitem__ ,则 iter() 创建一个迭代器,尝试按索引获取项目,从 0(零)开始。
  3. 如果失败,Python 会抛出 TypeError,通常是‘“C object is not iterable,”’(C 对象不可迭代),其中 C 是目标对象的类。

这就是为什么所有 Python 序列都是可迭代的:根据定义,它们都实现了 __getitem__。事实上,标准序列也实现了 __iter__,用户自定义序列也应该如此,因为通过 __getitem__ 进行迭代是为了向后兼容,将来可能会消失——尽管从 Python 3.10 开始它没有被弃用,我怀疑它是否会被删除。

正如“Python Digs Sequences”中提到的,这是鸭子类型的一种极端形式:一个对象不仅在实现特殊方法 __iter__ 时被认为是可迭代的,而且在它实现 __getitem__ 时也被认为是可迭代的。举个例子:

>>> class Spam:
...     def __getitem__(self, i):
...         print('->', i)
...         raise IndexError()
...
>>> spam_can = Spam()
>>> iter(spam_can)
<iterator object at 0x10a878f70>
>>> list(spam_can)
-> 0
[]
>>> from collections import abc
>>> isinstance(spam_can, abc.Iterable)
False

如果类提供 __getitem__方法,则 iter() 内置函数接受该类的一个实例作为可迭代对象,并从该实例构建一个迭代器。Python 的迭代机制将从索引0 开始调用 __getitem__,并将 IndexError 作为迭代完成的信号。

请注意,尽管 spam_can 是可迭代的(它的 __getitem__ 可以提供项),但它不能被 isinstance 识别为abc.Iterable 。

在天鹅类型方法中,iterable 的定义更简单但不够灵活:如果一个对象实现了 __iter__ 方法,则它被认为是可迭代的。不需要进行子类化或注册,因为 abc.Iterable 实现了 __subclasshook__,如“使用 ABC 的结构类型”中所见。这是一个示例:

>>> class GooseSpam:
...     def __iter__(self):
...         pass
...
>>> from collections import abc
>>> issubclass(GooseSpam, abc.Iterable)
True
>>> goose_spam_can = GooseSpam()
>>> isinstance(goose_spam_can, abc.Iterable)
True

TIP:

从 Python 3.10 开始,检查对象 x 是否可迭代的最准确方法是调用 iter(x) 并在不是的情况下处理 TypeError 异常。这比使用 isinstance(x, abc.Iterable) 更准确,因为 iter(x) 也考虑了遗留的 __getitem__ 方法的机制,而 Iterable ABC 没有。

如果在检查后立即迭代对象,那么显式检查对象是否可迭代可能是没有必要的。毕竟,当尝试对不可迭代对象进行迭代时,Python 抛出的异常就很清楚了TypeError: 'C' object is not iterable.如果除了抛出TypeError异常之外还要做进一步的处理,那么可以使用 try/except 块,而不是进行显式检查。如果对象才后面才会进行迭代,则显式检查可能有意义;在这种情况下,尽早捕获错误会使调试更容易。

Python 本身比我们自己的代码更常使用 iter() 内置函数。我们还有第二种使用它的方法,但它并不广为人知。

将 iter 与可调用对象一起使用

我们可以使用两个参数调用 iter() 以从函数或任何可调用对象创建迭代器。在这种用法中,第一个参数必须是可重复调用(不能带参数)以生成值的可调用对象,第二个参数是哨兵:一个标记值,当由可调用对象返回时,它会导致迭代器引发 StopIteration 而不是生成哨兵值。

以下示例显示如何使用 iter 掷六面骰子直到掷出 1为止:

>>> def d6():
...     return randint(1, 6)
...
>>> d6_iter = iter(d6, 1)
>>> d6_iter
<callable_iterator object at 0x10a245270>
>>> for roll in d6_iter:
...     print(roll)
...
4
3
6
3

注意这里的 iter 函数返回一个 callable_iterator。示例中的 for 循环可能会运行很长时间,但它永远不会显示 1,因为这是哨兵值。与迭代器一样,示例中的 d6_iter 对象一旦迭代完成就不能再次迭代。要重新开始,我们必须通过再次调用 iter() 来重建迭代器。

iter 的文档包括以下说明和示例代码。

第二种形式的 iter() 的一个有用应用是构建一个块阅读器。例如,从二进制数据库文件中读取固定宽度的块,直到到达文件末尾:

from functools import partial

with open('mydata.db', 'rb') as f:
    read64 = partial(f.read, 64)
    for block in iter(read64, b''):
        process_block(block)

为清楚起见,我添加了 给read64 赋值的语句,这不在原始示例中。parial() 函数是必需的,因为提供给 iter() 的可调用对象必须不带参数。在示例中,空字节对象是哨兵,因为当没有更多字节要读取时,f.read 会返回该对象。

下一节将详细介绍可迭代对象和迭代器之间的关系。

可迭代对象迭代器与迭代器

从“为什么序列是可迭代的:迭代器函数”中的解释我们可以推断出一个定义:

iterable:

iter 内置函数可以从中获取迭代器的任何对象。实现 __iter__ 方法返回迭代器的对象是可迭代的。序列始终是可迭代的,实现接受基于 0 的索引 __getitem__ 方法的对象也是可迭代的。

明确迭代器和迭代器之间的关系很重要:Python 从迭代器中获取迭代器。

这是一个简单的 for 循环迭代 str 。 str 'ABC' 在这里是可迭代的。幕后有一个迭代器,但你没有看到它:

>>> s = 'ABC'
>>> for char in s:
...     print(char)
...
A
B
C

如果没有 for 语句并且我们不得不用一个 while 循环手动模拟 for 机器,这就是我们必须写的:

>>> s = 'ABC'
>>> it = iter(s)  1
>>> while True:
...     try:
...         print(next(it))  2
...     except StopIteration:  3
...         del it  4
...         break  5
...
A
B
C
  1. 从可迭代对象构建迭代器it。
  2. 在迭代器上重复调用 next 以获取下一项。
  3. 当没有其他项时,迭代器会抛出 StopIteration。
  4. 释放对迭代器的引用——迭代器对象被丢弃。
  5. 退出循环

StopIteration 表示迭代器已耗尽。此异常由内置的 iter() 内部处理,它是 for 循环和其他迭代上下文(如列表推导式、可迭代对象解包等)逻辑的一部分。

Python 的迭代器标准接口有两种方法:

__next__:

返回下一个可用的元素,如果没有元素,则抛出 StopIteration异常。
__iter__:

返回self;这允许在需要迭代的地方使用迭代器,例如在 for 循环中。

该接口在 collections.abc.Iterator ABC 中形式化,它声明了 __next__ 抽象方法,以及继承自 Iterable——其中声明了抽象 __iter__ 方法。请参见图 17-1。

collections.abc.Iterator 的源代码在例 17-3 中。 

例 17-3。 abc.Iterator 类;摘自 Lib/_collections_abc.py

class Iterator(Iterable):

    __slots__ = ()

    @abstractmethod
    def __next__(self):
        'Return the next item from the iterator. When exhausted, raise StopIteration'
        raise StopIteration

    def __iter__(self):
        return self

    @classmethod
    def __subclasshook__(cls, C):  1
        if cls is Iterator:
            return _check_methods(C, '__iter__', '__next__')  2
        return NotImplemented
  1. __subclasshook__ 支持使用 isinstance 和 issubclass 进行结构类型检查。我们在“Structural typing with ABCs”中介绍过这个方法。
  2. _check_methods 遍历类C的 __mro__ 以检查方法是否在其基类中实现。它在同一个 Lib/_collections_abc.py 模块中定义。如果实现了这些方法,C 类将被识别为 Iterator 的虚拟子类。换句话说, issubclass(C, Iterable) 将返回 True。

Warning:

Iterator ABC 中定义的抽象方法是 Python 3 中的 it.__next__() 和 Python 2 中的 it.next()。一如既往,您应该避免直接调用特殊方法。只需使用next(it):这个内置函数在 Python 2 和 3 中做正确的事情——这对于那些将代码库从 2 迁移到 3 的人很有用。

Python 3.9 中的 Lib/types.py 模块源代码有一条注释说:

# Iterators in Python aren't a matter of type but of protocol.  A large
# and changing number of builtin types implement *some* flavor of
# iterator.  Don't check the type!  Use hasattr to check for both
# "__iter__" and "__next__" attributes instead.

事实上,这正是 abc.Iterator ABC 的 __subclasshook__ 方法所做的。

TIP:

根据 Lib/types.py 的建议和 Lib/_collections_abc.py 中实现的逻辑,检查对象 x 是否为迭代器的最佳方法是调用 isinstance(x, abc.Iterator)。由于 Iterator.__subclasshook__,即使 x 的类不是 Iterator 的真实或虚拟子类,这个测试也能进行检查。

回到示例 17-1 中的 Sentence 类,您可以使用 Python 控制台清楚地看到迭代器是如何由 iter() 构建并由 next() 使用的:

>>> s3 = Sentence('Life of Brian')  1
>>> it = iter(s3)  2
>>> it  # doctest: +ELLIPSIS
<iterator object at 0x...>
>>> next(it)  3
'Life'
>>> next(it)
'of'
>>> next(it)
'Brian'
>>> next(it)  4
Traceback (most recent call last):
  ...
StopIteration
>>> list(it)  5
[]
>>> list(iter(s3))  6
['Life', 'of', 'Brian']
  1. 用三个词创建一个sentence实例 s3。
  2. 从 s3 获取迭代器。
  3. next(it) 获取下一个单词。
  4. 没有更多的单词,所以迭代器抛出 StopIteration 异常。
  5. 一旦耗尽,迭代器将始终抛出 StopIteration,这使它看起来像是空的。
  6. 要再次迭代这个sentence对象,必须构建一个新的迭代器

因为迭代器只需要 __next__ 和 __iter__两个方法,所以除了调用 next() 并捕获 StopIteration 之外,没有办法检查是否还有剩余的元素。此外,不可能“重置”迭代器。如果您需要重新开始迭代一个迭代器,则需要在最初构建迭代器的可迭代对象上调用 iter()。对迭代器本身调用 iter() 不会有重置迭代器,因为——如前所述——Iterator.__iter__ 返回 self ,所以这不会重置耗尽的迭代器。

这个最小的接口的设计是合理的,因为实际上并不是所有的迭代器都是可重置的。例如,如果迭代器正在从网络读取数据包,则无法对它进行重置。

示例 17-1 中 Sentence 的第一个版本是可迭代的,这要归功于 iter() 内置对序列的特殊处理。接下来,我们将实现实现 __iter__ 以返回迭代器的 Sentence 变体。

实现 __iter__ 的Sentence类

Sentence 的下一个变体实现了标准的可迭代协议,首先通过实现迭代器设计模式,然后使用生成器函数。

Sentence#2:经典迭代器

下一个 Sentence 实现遵循 Design Patterns 书中经典 Iterator 设计模式的蓝图。请注意,它不是 Python的常见做法,因为接下来的重构将说明原因。但是这一版可以说明可迭代集合和使用它的迭代器之间的区别。

示例 17-4 中的 Sentence 类是可迭代的,因为它实现了 __iter__ 特殊方法,该方法构建并返回一个 SentenceIterator。这就是迭代器和迭代器的关系。

例 17-4。 sentence_iter.py:使用迭代器模式实现的Sentence类

import re
import reprlib

RE_WORD = re.compile(r'\w+')


class Sentence:

    def __init__(self, text):
        self.text = text
        self.words = RE_WORD.findall(text)

    def __repr__(self):
        return f'Sentence({reprlib.repr(self.text)})'

    def __iter__(self):  1
        return SentenceIterator(self.words)  2


class SentenceIterator:

    def __init__(self, words):
        self.words = words  3
        self.index = 0  4

    def __next__(self):
        try:
            word = self.words[self.index]  5
        except IndexError:
            raise StopIteration()  6
        self.index += 1  7
        return word  8

    def __iter__(self):  9
        return self
  1. __iter__ 方法是对先前 Sentence 实现的唯一补充。此版本没有 __getitem__,以表明该类是可迭代的,因为它实现了 __iter__。
  2. __iter__ 通过实例化并返回一个迭代器来实现可迭代协议。
  3. SentenceIterator 持有对words列表的引用
  4. self.index 确定要获取的下一个单词的索引。
  5. 在 索引self.index 获取单词。
  6. 如果 self.index 索引没有对应单词,则抛出 StopIteration异常。
  7. self.index 自增1
  8. 返回word
  9. 实现 self.__iter__。

示例 17-4 中的代码通过了示例 17-2 中的测试。

请注意,此示例实际上并不需要在 SentenceIterator 中实现 __iter__ ,但这样做是正确的:迭代器应该同时实现 __next__ 和 __iter__,这样做可以让迭代器通过 issubclass(SentenceIterator, abc.Iterator) 测试。如果我们从 abc.Iterator 继承 SentenceIterator,我们将继承具体的 abc.Iterator.__iter__ 方法。

这会带来大量的工作(无论如何,对于我们这些被宠坏的 Python 程序员来说)。请注意 SentenceIterator 中的大多数代码如何管理迭代器的内部状态。很快我们就会看到如何进行简化。但首先,简要介绍一个看似合理实则错误的实现捷径。

不要让 可迭代对象 成为它自己的迭代器

构建迭代器和迭代器时出错的一个常见原因是混淆了两者。需要明确的是:迭代器有一个 __iter__ 方法,每次都会实例化一个新的迭代器。迭代器实现了一个返回单个元素的 __next__ 方法和一个返回 self 的 __iter__ 方法。

因此,迭代器也是可迭代的,但可迭代的对象不是迭代器。

除了在 Sentence 类中实现 __iter__ 之外,你还可能想实现 __next__,从而使每个 Sentence 实例同时成为可迭代对象和迭代器。但这很少是一个好主意。根据在 Google 审查 Python 代码的丰富经验的 Alex Martelli 的说法,这也是一种常见的反模式。

GoF 书中迭代器设计模式的“适用性”部分  说:

迭代器模式可用于:

  • 在不暴露其内部内容的情况下访问聚合对象的内容。
  • 支持聚合对象的多次遍历。
  • 为遍历不同的聚合结构提供统一的接口(即支持多态迭代)。

为了“支持多次遍历”,必须可以从同一个可迭代实例中获得多个独立的迭代器,并且每个迭代器必须保持自己的内部状态,所以模式的正确实现需要每次调用 iter(my_iterable) 来创建一个新的、独立的迭代器。这就是我们在这个例子中需要 SentenceIterator 类的原因。

现在经典的 Iterator 模式已经正确演示,可以告一段落。 Python 结合了 Barbara Liskov 的 CLU 语言中的 yield 关键字,因此我们不需要“手动生成”代码来实现迭代器。

接下来的部分介绍了符合python习惯的 Sentence 。


Sentence Take #3:生成器函数

相同功能的 Pythonic 实现使用了生成器,避免了实现 SentenceIterator 类。示例 17-5 之后对生成器进行了正确的解释。
例 17-5。 sentence_gen.py:使用生成器实现Sentence类

import re
import reprlib

RE_WORD = re.compile(r'\w+')


class Sentence:

    def __init__(self, text):
        self.text = text
        self.words = RE_WORD.findall(text)

    def __repr__(self):
        return 'Sentence(%s)' % reprlib.repr(self.text)

    def __iter__(self):
        for word in self.words:  1
            yield word  2
        3

# done! 4
  1. 迭代 self.words
  2. 生成当前的word。
  3. 不需要显式返回一个返回值;这个方法可以直接退出并自动返回。无论哪种方式,生成器函数都不会抛出 StopIteration:它只是在完成生成值后退出
  4. 不再需要单独的迭代器类!

同样,我们有一个不同的 Sentence 实现,它通过了示例 17-2 中的测试。

回到示例 17-4 中的 Sentence 代码,__iter__ 调用 SentenceIterator 构造函数来构建并返回一个迭代器。现在示例 17-5 中的迭代器实际上是一个生成器对象,在调用 __iter__ 方法时自动构建,因为在这里 __iter__ 是一个生成器函数。
下面是生成器的完整解释。

生成器的工作原理

任何在其主体中包含 yield 关键字的 Python 函数都是生成器函数:调用这个函数返回一个生成器对象。换句话说,一个生成器函数就是一个生成器工厂。

TIP
将普通函数与生成器函数区分开来的唯一语法是后者在其主体有 yield 关键字。有些人认为应该使用像 gen 这样的新关键字而不是 def 来声明生成器函数,但 Guido 不同意。他的论点在  PEP 255 — Simple Generators 中。

这是用于演示生成器行为的最简单的函数:

>>> def gen_123():
...     yield 1  1
...     yield 2
...     yield 3
...
>>> gen_123  # doctest: +ELLIPSIS
<function gen_123 at 0x...>  2
>>> gen_123()   # doctest: +ELLIPSIS
<generator object gen_123 at 0x...>  3
>>> for i in gen_123():  4
...     print(i)
1
2
3
>>> g = gen_123()  5
>>> next(g)  6
1
>>> next(g)
2
>>> next(g)
3
>>> next(g)  7
Traceback (most recent call last):
  ...
StopIteration
  1. 生成器函数的主体通常将yield放入循环内,但不一定总是这样;这个例子,我写了三次 yield语句。
  2. 仔细观察,我们看到 gen_123 是一个函数对象。
  3. 但是调用后, gen_123() 返回一个生成器对象。
  4. 生成器对象实现了 Iterator 接口,因此它们也是可迭代的。
  5. 我们将这个新的生成器对象赋值给 g,因此我们可以对g进行试验。
  6. 因为 g 是一个迭代器,所以调用 next(g) 会获取由 yield 生成的下一个元素。
  7. 当生成器函数返回时,生成器对象抛出 StopIteration。

生成器函数构建一个生成器对象,该对象包装函数体。当我们在生成器对象上调用 next() 时,执行前进到函数体中的下一个 yield,next() 调用返回为函数挂起时yield生成的值。最后,根据迭代器协议,当函数体返回时,由 Python 创建的封闭生成器对象抛出 StopIteration异常。


TIP:

我发现在谈论从生成器获得的值时保持严谨是有帮助的。说生成器“返回”值是令人困惑的。调用生成器函数返回一个生成器。生成器生成值。生成器不会以通常的方式“返回”值:生成器函数体中的 return 语句会导致生成器对象抛出 StopIteration异常。如果在生成器结尾编写return x,调用者可以从 StopIteration 异常中检索 x 的值,但通常这是使用 yield from 语法自动完成的,我们将在“从协程返回值”中看到。


示例 17-6 使 for 循环和函数体之间的交互更加明确。

例 17-6。运行时打印消息的生成器函数

>>> def gen_AB():
...     print('start')
...     yield 'A'          1
...     print('continue')
...     yield 'B'          2
...     print('end.')      3
...
>>> for c in gen_AB():     4
...     print('-->', c)    5
...
start     6
--> A     7
continue  8
--> B     9
end.      10
>>>       11
  1. for 循环中对 next() 的第一次隐式调用将打印“start”并在第一个 yield 处停止,生成值“A”。
  2. for 循环中对 next() 的第二次隐式调用将打印 'continue' 并在第二个 yield 处停止,生成值 'B'。
  3. 第三次调用 next() 将打印“end.”。并落入函数体的末尾,导致生成器对象抛出 StopIteration异常。
  4. 为了迭代,for 循环的执行机制等效于 g = iter(gen_AB()) 的操作以获取生成器对象,然后在每次迭代时使用 next(g)。
  5. 循环打印 --> 和 next(g) 返回的值。此输出仅在生成器函数内的print调用输出之后才会出现。
  6. 文本 start 来自生成器主体中的 print('start') 。
  7. 生成器主体中的 yield 'A' 生成 for 循环消耗的值 A,该值被分配给 c 变量并输出 --> A。
  8. 迭代继续第二次调用 next(g),将生成器主体从 yield 'A' 推进到 yield 'B'。文本 continue 由生成器主体中的第二个打印输出。
  9. yield 'B' 生成 for 循环消费的值 B,该值被分配给 c 循环变量,因此循环打印 --> B。
  10. 迭代继续第三次调用 next(it),前进到函数体的末尾。文本end.由于生成器函数定义体中的第三个print函数输出。
  11. 当生成器函数运行到最后时,生成器对象会抛出 StopIteration异常。 for 循环机制捕获该异常,并且循环终止时没有报错。

现在例 17-5 中的 Sentence.__iter__ 是如何工作的很清楚了:__iter__ 是一个生成器函数,它在调用时会构建一个实现迭代器接口的生成器对象,因此不再需要 SentenceIterator 类。

第二个版本的 Sentence 比第一个更简洁,但也没有想象中的那么懒惰。如今,至少在编程语言和 API 中,懒惰被认为是一个很好的特征。惰性实现将尽可能延后生产值。这可以节省内存,也可以避免浪费 CPU 周期。

接下来我们以惰性方式定义Sentence类
 

惰性的Sentence

Sentence 的最终变体是惰性的,利用了 re 模块中的惰性函数。

Sentence类第四版:惰性实现

Iterator 接口被设计为惰性的:next(my_iterator) 一次生成一个元素。懒惰的反面是急迫:惰性求值和及早求值是编程语言理论中的技术术语。

到目前为止,我们的 Sentence 实现并不具备惰性,因为 __init__ 急切地构建了文本中所有单词的列表,并将其绑定到 self.words 属性。这需要处理整个文本,并且列表可能使用与文本本身一样多的内存(可能更多;这取决于文本中有多少非单词字符)。如果用户只迭代前几个词,那么大部分工作将是徒劳的。如果你想知道“在 Python 中是否有一种懒惰的方法来做到这一点?”,答案通常是“是的”。

re.finditer 函数是 re.findall 的惰性版本。 re.finditer 返回一个生成器,根据需要生成 re.MatchObject 实例,而不是列表。如果有很多匹配项,re.finditer 会节省大量内存。使用它,我们的 Sentence 的第三个版本现在是惰性的:它只在需要时从文本中读取下一个单词。代码在示例 17-7 中。
例 17-7。 sentence_gen2.py:在生成器函数中调用 re.finditer 生成器函数的实现的Sentence
 

import re
import reprlib

RE_WORD = re.compile(r'\w+')


class Sentence:

    def __init__(self, text):
        self.text = text  1

    def __repr__(self):
        return f'Sentence({reprlib.repr(self.text)})'

    def __iter__(self):
        for match in RE_WORD.finditer(self.text):  2
            yield match.group()  3

  1. 不需要保存单词的列表
  2. finditer 在 self.text 上的 RE_WORD 匹配上构建迭代器,生成MatchObject 实例。
  3. match.group() 从 MatchObject 实例中提取匹配的文本。

生成器已经极大的简化了代码,但是使用生成器表达式可以使代码更加简洁。


Sentence类第五版:生成器表达式

我们可以用生成器表达式替换简单的生成器函数,例如上一个 Sentence 类(示例 17-7)中的生成器函数。正如列表推导式构建列表一样,生成器表达式构建生成器对象。示例 17-8 对比了他们的行为。

例 17-8。 列表推导式使用gen_AB 生成器函数,然后由生成器表达式使用

>>> def gen_AB():  1
...     print('start')
...     yield 'A'
...     print('continue')
...     yield 'B'
...     print('end.')
...
>>> res1 = [x*3 for x in gen_AB()]  2
start
continue
end.
>>> for i in res1:  3
...     print('-->', i)
...
--> AAA
--> BBB
>>> res2 = (x*3 for x in gen_AB())  4
>>> res2
<generator object <genexpr> at 0x10063c240>
>>> for i in res2:  5
...     print('-->', i)
...
start      6
--> AAA
continue
--> BBB
end.
  1. 这与示例 17-6 中的 gen_AB 函数相同。
  2. 列表推导式迫切地迭代由 gen_AB() 返回的生成器对象生成的元素:'A' 和 'B'。注意下几行的输出:start、continue、end。
  3. 这个 for 循环遍历列表推导式构建的 res1 列表。
  4. 生成器表达式返回 res2,一个生成器对象。这里没有消费这个生成器对象。
  5. 只有当 for 循环遍历 res2 时,此生成器才会从 gen_AB 中获取元素。for 循环的每次迭代都隐式调用 next(res2),后者又对 gen_AB() 返回的生成器对象调用 next(),前进到下一个 yield语句。
  6. 请注意 gen_AB() 的输出如何与 for 循环中的打印输出交错。

我们可以使用生成器表达式来进一步减少 Sentence 类中的代码。请参见示例 17-9。
例 17-9。 sentence_genexp.py:使用生成器表达式实现的Sentence类
 

import re
import reprlib

RE_WORD = re.compile(r'\w+')


class Sentence:

    def __init__(self, text):
        self.text = text

    def __repr__(self):
        return f'Sentence({reprlib.repr(self.text)})'

    def __iter__(self):
        return (match.group() for match in RE_WORD.finditer(self.text))

 与示例 17-7 的唯一区别是 __iter__ 方法,它在这里不是一个生成器函数(它没有 yield)而是使用生成器表达式来构建生成器然后返回它。最终结果是一样的:__iter__ 返回一个生成器对象。

生成器表达式是语法糖:它们总是可以被生成器函数替换,但有时更方便。下一节是关于生成器表达式的使用。

何时使用生成器表达式

在示例 12-16 中实现 Vector 类时,我使用了几个生成器表达式。 __eq__、__hash__、__abs__、angle、angles、format、__add__ 和 __mul__ 方法中的每一个都有一个生成器表达式。在所有这些方法中,列表推导式也可以工作,代价是使用更多内存来存储中间列表值。

在示例 17-9 中,我们看到生成器表达式是一种无需定义和调用函数即可创建生成器的语法快捷方式。另一方面,生成器函数更灵活:我们可以用多条语句编写复杂的逻辑,甚至可以将它们用作协程,正如我们将在“经典协程”中看到的那样。

对于更简单的情况,生成器表达式一目了然更容易阅读,如 Vector 示例所示。我选择要使用的语法的经验法则很简单:如果生成器表达式需要多行,我更喜欢编写生成器函数以提高可读性。


SYNTAX TIP

当生成器表达式作为单个参数传递给函数或构造函数时,您不需要为函数调用编写一组括号,并用另一个括号将生成器表达式括起来。编写一堆括号就可以了,就像示例 12-16 中 __mul__ 方法的 Vector 调用一样,复制在这里:

def __mul__(self, scalar):
    if isinstance(scalar, numbers.Real):
        return Vector(n * scalar for n in self)
    else:
        return NotImplemented

但是,如果生成器表达式后有其它的参数,则需要将其括在括号中以避免出现 SyntaxError。


我们看到的 Sentence 示例演示了生成器扮演经典迭代器模式的角色:从集合中获取元素。但是我们也可以使用生成器来生成独立于数据源的值。下一节会举例说明。

但首先,简要讨论迭代器和生成器的概念的重叠部分。


对比迭代器和生成器

在官方 Python 文档和代码库中,有关迭代器和生成器的术语不一致且不断发展。我采用了以下定义:

iterator:

        实现 .__next__() 方法的任何对象的通用术语。迭代器旨在生成由客户端代码使用的数据——即通过 for 循环或其他迭代功能驱动迭代器的代码,或通过在迭代器上显式调用 next(it)尽管——这种显式用法不太常见。实际上,我们在 Python 中使用的大多数迭代器都是生成器。

        
generator:

        由 Python 编译器构建的迭代器。要创建一个生成器,我们不需要实现 .__next__()。 而是使用 yield 关键字来创建一个生成器函数,它是生成器对象的工厂。生成器表达式是另一种构建生成器对象的方法。生成器对象提供 .__next__()方法,所以它们是迭代器。从 Python 3.5 开始,我们也有使用 async def 声明的异步生成器。我们将在第 21 章——异步编程中研究它们。

Python 词汇表最近引入了术语生成器迭代器来指代由生成器函数构建的生成器对象,而生成器表达式的条目表示它返回一个“迭代器”。 但是根据 Python 的说法,这两种情况下返回的对象都是生成器对象:   

>>> def g():
...     yield 0
...
>>> g()
<generator object g at 0x10e6fb290>
>>> ge = (c for c in 'XYZ')
>>> ge
<generator object <genexpr> at 0x10e936ce0>
>>> type(g()), type(ge)
(<class 'generator'>, <class 'generator'>)

另一个示例:等差数列生成器

经典的迭代器模式都是关于遍历:概览一个数据结构。不过,当不是从集合中获取元素,而是动态的获取序列中即时生成的值时,基于方法的标准接口也很有用。例如,内置的 range 生成有穷整数等差数列 (AP)。如果您需要生成的是不仅仅是整数类型的数字的 AP,而是任意数字类型的,该怎么办?

示例 17-10 显示了我们稍后将看到的 ArithmeticProgression 类的一些控制台测试。示例 17-10 中构造函数的签名是 ArithmeticProgression(begin, step[, end])。内置范围的完整签名是 range(start, stop[, step])。我选择实现不同的签名,因为step是强制指定的,而 end 在等差数列中是可选的。我还将参数名称从start/stop更改为begin/end,以明确我选择了不同的签名。在示例 17-10 中的每个测试中,我对结果调用 list() 以检查生成的值。

例 17-10。 ArithmeticProgression 类的演示

    >>> ap = ArithmeticProgression(0, 1, 3)
    >>> list(ap)
    [0, 1, 2]
    >>> ap = ArithmeticProgression(1, .5, 3)
    >>> list(ap)
    [1.0, 1.5, 2.0, 2.5]
    >>> ap = ArithmeticProgression(0, 1/3, 1)
    >>> list(ap)
    [0.0, 0.3333333333333333, 0.6666666666666666]
    >>> from fractions import Fraction
    >>> ap = ArithmeticProgression(0, Fraction(1, 3), 1)
    >>> list(ap)
    [Fraction(0, 1), Fraction(1, 3), Fraction(2, 3)]
    >>> from decimal import Decimal
    >>> ap = ArithmeticProgression(0, Decimal('.1'), .3)
    >>> list(ap)
    [Decimal('0'), Decimal('0.1'), Decimal('0.2')]

请注意,根据 Python 算术的数字强制规则,结果等差数列中的数字类型与 begin + step 的类型一致。在示例 17-10 中,您会看到 int、float、Fraction 和 Decimal 数的列表。

示例 17-11 展示了 ArithmeticProgression 类的实现。

class ArithmeticProgression:

    def __init__(self, begin, step, end=None):       1
        self.begin = begin
        self.step = step
        self.end = end  # None -> "infinite" series

    def __iter__(self):
        result_type = type(self.begin + self.step)   2
        result = result_type(self.begin)             3
        forever = self.end is None                   4
        index = 0
        while forever or result < self.end:          5
            yield result                             6
            index += 1
            result = self.begin + self.step * index  7
  1. __init__ 需要两个参数:begin 和 step; end 是可选的,如果它是 None,那么生成的是无穷数列
  2. 获取 self.begin + self.step 的结果的类型。例如,如果一个是 int 而另一个是 float,则 result_type 将为 float。
  3. 这一行使result与 self.begin 的数值相同,但被强制转换为后续加法的结果类型
  4. 为了可读性,如果 self.end 属性为 None,则永久标志将为 True,从而生成的是无穷数列。
  5. 这个循环永远运行或直到result >= self.end。当这个循环退出时,函数也会退出。
  6. 生成当前的result。
  7. 计算下一个可能的result。它可能永远不会生成,因为 while 循环可能会终止。

在示例 17-11 的最后一行,我没有在每次循环时将 self.step 和前一个result相加,而是选择忽略前一个result,并生成新的result,方法是将 self.begin + self.step * index。这避免了连续加法后浮点数的累积效应导致错误。下面简单的实验可以清晰的展示差异:

>>> 100 * 1.1
110.00000000000001
>>> sum(1.1 for _ in range(100))
109.99999999999982
>>> 1000 * 1.1
1100.0
>>> sum(1.1 for _ in range(1000))
1100.0000000000086

示例 17-11 中的 ArithmeticProgression 类按预期工作,并且是使用生成器函数实现 __iter__ 特殊方法的另一个示例。但是,如果类的全部意义是通过实现 __iter__ 来构建生成器,我们可以将类替换为生成器函数。毕竟,一个生成器函数是一个生成器工厂。

示例 17-12 显示了一个名为 aritprog_gen 的生成器函数,它执行与 ArithmeticProgression 相同的工作,但代码更少。如果您只调用 aritprog_gen 而不是 ArithmeticProgression,则示例 17-10 中的测试全部通过.


例 17-12。 aritprog_gen 生成器函数

def aritprog_gen(begin, step, end=None):
    result = type(begin + step)(begin)
    forever = end is None
    index = 0
    while forever or result < end:
        yield result
        index += 1
        result = begin + step * index

示例 17-12 很优雅,但请始终记住:标准库中有大量拿来即用的生成器,下一节将展示使用 itertools 模块的更简短的实现。

使用 itertools 生成等差数列

Python 3.10 中的 itertools 模块有 20 个生成器函数,并可以以各种有趣的方式组合。

例如, itertools.count 函数返回一个生成数字的生成器。如果不传入参数,它产生从 0 开始的整数序列。但是您可以提供可选的begin和step参数来实现类似于我们的 aritprog_gen 函数的结果。

>>> import itertools
>>> gen = itertools.count(1, .5)
>>> next(gen)
1
>>> next(gen)
1.5
>>> next(gen)
2.0
>>> next(gen)
2.5

Warning:

itertools.count 永远不会停止,因此如果您调用 list(count()),Python 将尝试构建一个占满内存的大列表。实际上,在调用失败之前很久,电脑会疯狂的运转。

另一方面,标准库提供了 itertools.takewhile 函数:它返回一个生成器,该生成器消耗另一个生成器并在给定条件计算结果为为 False 时停止。所以我们可以将两者结合起来使用:
 

>>> gen = itertools.takewhile(lambda n: n < 3, itertools.count(1, .5))
>>> list(gen)
[1, 1.5, 2.0, 2.5]

利用 takewhile 和 count,示例 17-13 更加简洁。

例 17-13。 aritprog_v3.py:这与之前的 aritprog_gen 函数类似

import itertools


def aritprog_gen(begin, step, end=None):
    first = type(begin + step)(begin)
    ap_gen = itertools.count(first, step)
    if end is None:
        return ap_gen
    return itertools.takewhile(lambda n: n < end, ap_gen)

请注意,示例 17-13 中的 aritprog_gen 不是生成器函数:它的主体中没有 yield。但它返回一个生成器,就像生成器函数一样。

但是,请记住 itertools.count 重复递增step的值,因此它生成的浮点数序列的精确度不如示例 17-12 。

示例 17-13 的要点是:在实现生成器时,要知道标准库中有什么可用,否则很有可能你会重新发明轮子。这就是为什么下一节介绍几个现成的生成器函数的原因。

标准库中的生成器函数

Note:

也许您知道本节中提到的所有功能,但其中一些功能未得到充分利用,因此快速概览可能有助于回忆一下已有的功能。

标准库提供了许多生成器,从提供逐行迭代的纯文本文件对象,到出色的 os.walk 函数,它在遍历目录树时生成文件名,使递归文件系统搜索像 for 循环一样简单。

os.walk 生成器函数令人印象深刻,但在本节中,我想重点介绍通用的函数,这些函数将任意可迭代对象作为参数并返回生成选定的、计算出的或重新排列项目的生成器。在下表中,我总结了其中的24个,分别来自内置、itertools 和 functools 模块。为方便起见,我按高级功能对它们进行了分组,而不管它们在何处定义。

第一组是过滤生成器函数:它们生成由输入可迭代对象产出元素的子集,而不改变元素本身。与 takewhile 一样,表 17-1 中列出的大多数函数都接受一个断言参数(predicate),这是一个单参数布尔函数,将应用于输入中的每个元素以确定该元素是否含在输出中。

Table 17-1. Filtering generator functions
ModuleFunctionDescription

itertools

compress(it, selector_it)

Consumes two iterables in parallel; yields items from it whenever the corresponding item in selector_it is truthy

itertools

dropwhile(predicate, it)

Consumes it skipping items while predicate computes truthy, then yields every remaining item (no further checks are made)

(built-in)

filter(predicate, it)

Applies predicate to each item of iterable, yielding the item if predicate(item) is truthy; if predicate is None, only truthy items are yielded

itertools

filterfalse(predicate, it)

Same as filter, with the predicate logic negated: yields items whenever predicate computes falsy

itertools

islice(it, stop) or islice(it, start, stop, step=1)

Yields items from a slice of it, similar to s[:stop] or s[start:stop:step] except it can be any iterable, and the operation is lazy

itertools

takewhile(predicate, it)

Yields items while predicate computes truthy, then stops and no further checks are made

示例 17-14 中的控制台清单显示了表 17-1 中所有函数的使用。
例 17-14。过滤生成器函数示例

>>> def vowel(c):
...     return c.lower() in 'aeiou'
...
>>> list(filter(vowel, 'Aardvark'))
['A', 'a', 'a']
>>> import itertools
>>> list(itertools.filterfalse(vowel, 'Aardvark'))
['r', 'd', 'v', 'r', 'k']
>>> list(itertools.dropwhile(vowel, 'Aardvark'))
['r', 'd', 'v', 'a', 'r', 'k']
>>> list(itertools.takewhile(vowel, 'Aardvark'))
['A', 'a']
>>> list(itertools.compress('Aardvark', (1, 0, 1, 1, 0, 1)))
['A', 'r', 'd', 'a']
>>> list(itertools.islice('Aardvark', 4))
['A', 'a', 'r', 'd']
>>> list(itertools.islice('Aardvark', 4, 7))
['v', 'a', 'r']
>>> list(itertools.islice('Aardvark', 1, 7, 2))
['a', 'd', 'a']

下一组是映射生成器:它们生成从输入可迭代对象或多个可迭代对象(map和starmap)中的各个元素进行计算,然后返回结果。表 17-2 中的生成器函数为输入迭代中的每个元产生一个结果。如果输入来自多个可迭代对象,则在第一个输入可迭代对象耗尽后立即停止输出。

Table 17-2. Mapping generator functions
ModuleFunctionDescription

itertools

accumulate(it, [func])

Yields accumulated sums; if func is provided, yields the result of applying it to the first pair of items, then to the first result and next item, etc.

(built-in)

enumerate(iterable, start=0)

Yields 2-tuples of the form (index, item), where index is counted from start, and item is taken from the iterable

(built-in)

map(func, it1, [it2, …, itN])

Applies func to each item of it, yielding the result; if N iterables are given, func must take N arguments and the iterables will be consumed in parallel

itertools

starmap(func, it)

Applies func to each item of it, yielding the result; the input iterable should yield iterable items iit, and func is applied as func(*iit)

示例 17-15 演示了 itertools.accumulate 的一些用法。

例 17-15。 itertools.accumulate 生成器函数示例

>>> sample = [5, 4, 2, 8, 7, 6, 3, 0, 9, 1]
>>> import itertools
>>> list(itertools.accumulate(sample))  1
[5, 9, 11, 19, 26, 32, 35, 35, 44, 45]
>>> list(itertools.accumulate(sample, min))  2
[5, 4, 2, 2, 2, 2, 2, 0, 0, 0]
>>> list(itertools.accumulate(sample, max))  3
[5, 5, 5, 8, 8, 8, 8, 8, 9, 9]
>>> import operator
>>> list(itertools.accumulate(sample, operator.mul))  4
[5, 20, 40, 320, 2240, 13440, 40320, 0, 0, 0]
>>> list(itertools.accumulate(range(1, 11), operator.mul))
[1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]  5
  1.  计算总和
  2. 计算最小值
  3. 计算最大值
  4. 计算乘积
  5. 计算1!到10!

表 17-2 的其余函数如例 17-16 所示。

例 17-16。映射生成器函数示例
 

>>> list(enumerate('albatroz', 1))  1
[(1, 'a'), (2, 'l'), (3, 'b'), (4, 'a'), (5, 't'), (6, 'r'), (7, 'o'), (8, 'z')]
>>> import operator
>>> list(map(operator.mul, range(11), range(11)))  2
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
>>> list(map(operator.mul, range(11), [2, 4, 8]))  3
[0, 4, 16]
>>> list(map(lambda a, b: (a, b), range(11), [2, 4, 8]))  4
[(0, 2), (1, 4), (2, 8)]
>>> import itertools
>>> list(itertools.starmap(operator.mul, enumerate('albatroz', 1)))  5
['a', 'll', 'bbb', 'aaaa', 'ttttt', 'rrrrrr', 'ooooooo', 'zzzzzzzz']
>>> sample = [5, 4, 2, 8, 7, 6, 3, 0, 9, 1]
>>> list(itertools.starmap(lambda a, b: b / a,
...     enumerate(itertools.accumulate(sample), 1)))  6
[5.0, 4.5, 3.6666666666666665, 4.75, 5.2, 5.333333333333333,
5.0, 4.375, 4.888888888888889, 4.5]
  1. 从1开始,为单词的字母编号
  2. 从0-10,计算整数的平方
  3. 并行地将两个迭代中的数字相乘:当最短的迭代结束时结果停止。
  4. 作用等同于内置的zip函数
  5. 根据单词中的位置重复单词中的每个字母,从 1 开始。
  6. 计算平均值

接下来,我们有一组合并生成器——所有这些都从输入的多个迭代中产出元素。chain 和 chain.from_iterable 按顺序(一个接一个)使用输入的可迭代对象而 product、zip 和 zip_longest 并行使用输入的可迭代对象。见表 17-3。

Table 17-3. Generator functions that merge multiple input iterables
ModuleFunctionDescription

itertools

chain(it1, …, itN)

Yield all items from it1, then from it2 etc., seamlessly

itertools

chain.from_iterable(it)

Yield all items from each iterable produced by it, one after the other, seamlessly; it be an iterable where the items are also iterables, for example, a list of tuples

itertools

product(it1, …, itN, repeat=1)

Cartesian product: yields N-tuples made by combining items from each input iterable like nested for loops could produce; repeat allows the input iterables to be consumed more than once

(built-in)

zip(it1, …, itN, strict=False)

Yields N-tuples built from items taken from the iterables in parallel, silently stopping when the first iterable is exhausted, unless strict=True is givena

itertools

zip_longest(it1, …, itN, fillvalue=None)

Yields N-tuples built from items taken from the iterables in parallel, stopping only when the last iterable is exhausted, filling the blanks with the fillvalue

严格的仅关键字参数在 Python 3.10 中是新的。当strict=True 时,如果可迭代对象的长度不同,就会抛出ValueError异常。为了向后兼容,默认值为 False。 

示例 17-17 展示了 itertools.chain 和 zip 生成器函数及其兄弟函数的使用。回想一下 zip 函数是以拉链或拉链命名的(与压缩无关)。 zip 和 itertools.zip_longest 都是在“The Awesome zip”中介绍的。

例 17-17。合并生成器函数示例

>>> list(itertools.chain('ABC', range(2)))  1
['A', 'B', 'C', 0, 1]
>>> list(itertools.chain(enumerate('ABC')))  2
[(0, 'A'), (1, 'B'), (2, 'C')]
>>> list(itertools.chain.from_iterable(enumerate('ABC')))  3
[0, 'A', 1, 'B', 2, 'C']
>>> list(zip('ABC', range(5), [10, 20, 30, 40]))  4
[('A', 0, 10), ('B', 1, 20), ('C', 2, 30)]
>>> list(itertools.zip_longest('ABC', range(5)))  5
[('A', 0), ('B', 1), ('C', 2), (None, 3), (None, 4)]
>>> list(itertools.zip_longest('ABC', range(5), fillvalue='?'))  6
[('A', 0), ('B', 1), ('C', 2), ('?', 3), ('?', 4)]
  1. 调用chain时,通常传入两个或多个可迭代对象
  2. 如果只传入一个可迭代对象,那么chain函数没有什么用户
  3. 但是 chain.from_iterable 从可迭代对象中取出每个元素,并按顺序把元素连接起来,前提是每个元素本身都是可迭代的
  4. zip 可以并行使用任意数量的可迭代对象,但生成器总是在第一个可迭代对象结束后立即停止。在 Python ≥ 3.10 中,如果给出了 strict=True 参数并且一个可迭代对象在其他参数之前结束,则会抛出ValueError 异常。
  5. itertools.zip_longest 的工作方式与 zip 类似,不同之处在于它将所有输入的迭代器全部消费完,并根据需要用 None 填充输出元组。
  6. fillvalue 关键字参数指定自定义填充值。

itertools.product 生成器是一种计算笛卡尔积的惰性方法,我们使用“笛卡尔积”中包含多个 for 子句的列表推导式构建。带有多个 for 子句的生成器表达式也可用于惰性地生成笛卡尔积。示例 17-18 演示了 itertools.product。

例 17-18。 itertools.product 生成器函数示例

>>> list(itertools.product('ABC', range(2)))  1
[('A', 0), ('A', 1), ('B', 0), ('B', 1), ('C', 0), ('C', 1)]
>>> suits = 'spades hearts diamonds clubs'.split()
>>> list(itertools.product('AK', suits))  2
[('A', 'spades'), ('A', 'hearts'), ('A', 'diamonds'), ('A', 'clubs'),
('K', 'spades'), ('K', 'hearts'), ('K', 'diamonds'), ('K', 'clubs')]
>>> list(itertools.product('ABC'))  3
[('A',), ('B',), ('C',)]
>>> list(itertools.product('ABC', repeat=2))  4
[('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'B'),
('B', 'C'), ('C', 'A'), ('C', 'B'), ('C', 'C')]
>>> list(itertools.product(range(2), repeat=3))
[(0, 0, 0), (0, 0, 1), (0, 1, 0), (0, 1, 1), (1, 0, 0),
(1, 0, 1), (1, 1, 0), (1, 1, 1)]
>>> rows = itertools.product('AB', range(2), repeat=2)
>>> for row in rows: print(row)
...
('A', 0, 'A', 0)
('A', 0, 'A', 1)
('A', 0, 'B', 0)
('A', 0, 'B', 1)
('A', 1, 'A', 0)
('A', 1, 'A', 1)
('A', 1, 'B', 0)
('A', 1, 'B', 1)
('B', 0, 'A', 0)
('B', 0, 'A', 1)
('B', 0, 'B', 0)
('B', 0, 'B', 1)
('B', 1, 'A', 0)
('B', 1, 'A', 1)
('B', 1, 'B', 0)
('B', 1, 'B', 1)
  1. 具有三个字符的 str 和具有两个整数的范围的笛卡尔积产生六个元组(因为 3 * 2 是 6)。
  2. 两张牌('AK')和四套花色的乘积是由八元组组成的序列。
  3. 传入一个可迭代对象,product的结果是一系列只有一个元素的元组,不是很有用。
  4. repeat=N 关键字参数告诉 product 重复N次处理输入的可迭代对象 。

一些生成器函数通过为每个输入项产出多个值扩展输入的可迭代对象。它们在表 17-4 中列出。

Table 17-4. Generator functions that expand each input item into multiple output items
ModuleFunctionDescription

itertools

combinations(it, out_len)

Yield combinations of out_len items from the items yielded by it

itertools

combinations_with_replacement(it, out_len)

Yield combinations of out_len items from the items yielded by it, including combinations with repeated items

itertools

count(start=0, step=1)

Yields numbers starting at start, incremented by step, indefinitely

itertools

cycle(it)

Yields items from it storing a copy of each, then yields the entire sequence repeatedly, indefinitely

itertools

pairwise(it)

Yield successive overlapping pairs taken from the input iterable.a

itertools

permutations(it, out_len=None)

Yield permutations of out_len items from the items yielded by it; by default, out_len is len(list(it))

itertools

repeat(item, [times])

Yield the given item repeatedly, indefinitely unless a number of times is given

itertools.pairwise 是在 Python 3.10 中引入的

itertools 中的 count 和 repeat 函数返回生成器,这些生成器从无到有:它们都没有接收可迭代对象作为参数。

我们在“使用 itertools 的算术级数”中看到了 itertools.count。cycle生成器备份输入可迭代对象并重复产出对象的元素。示例 17-19 说明了count、cycle、pairwise和repeat的使用。

示例 17-19. count, cycle, pairwise和 repeat

>>> ct = itertools.count()  1
>>> next(ct)  2
0
>>> next(ct), next(ct), next(ct)  3
(1, 2, 3)
>>> list(itertools.islice(itertools.count(1, .3), 3))  4
[1, 1.3, 1.6]
>>> cy = itertools.cycle('ABC')  5
>>> next(cy)
'A'
>>> list(itertools.islice(cy, 7))  6
['B', 'C', 'A', 'B', 'C', 'A', 'B']
>>> list(itertools.pairwise(range(7)))  7
[(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)]
>>> rp = itertools.repeat(7)  8
>>> next(rp), next(rp)
(7, 7)
>>> list(itertools.repeat(8, 4))  9
[8, 8, 8, 8]
>>> list(map(operator.mul, range(11), itertools.repeat(5)))  10
[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50]
  1. 构建一个counter生成器 ct。
  2. 从 ct 中检索第一个元素
  3. 我无法从 ct 构建列表,因为 ct 是无穷的,所以我获取接下来的三个元素。
  4. 如果受 islice 或 takewhile 限制,我可以从counter生成器构建列表。
  5. 从“ABC”构建cycle生成器并获取第一个元素“A”。
  6. 一个list只能在islice限制的情况下才能创建;这里获取接下来的七个元素。
  7. 对于输入中的每个元素,只要下一个元素存在,pairwise 会生成一个包含该元素和下一个元素的元组。在 Python ≥ 3.10 中可用。
  8. 构建一个将永远生成数字 7 的repeat生成器。
  9. 重复生成器可以通过传递times参数来限制:这里数字 8 将生成 4 次。
  10. repeat 的一个常见用途:在 map 中提供一个固定的参数;这里它提供的乘数是5。

combinations、combinations_with_replacement 和permutations生成器函数——以及product——在 itertools 文档页面中被称为组合学生成器。itertools.product 和其他的组合学函数之间也有密切的关系,如例 17-20 所示。

例 17-20。组合学生成器函数为每个输入元素产出多个值

>>> list(itertools.combinations('ABC', 2))  1
[('A', 'B'), ('A', 'C'), ('B', 'C')]
>>> list(itertools.combinations_with_replacement('ABC', 2))  2
[('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'B'), ('B', 'C'), ('C', 'C')]
>>> list(itertools.permutations('ABC', 2))  3
[('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]
>>> list(itertools.product('ABC', repeat=2))  4
[('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'B'), ('B', 'C'),
('C', 'A'), ('C', 'B'), ('C', 'C')]
  1. 'ABC'中的元素的len()==2的所有组合;生成的元组中的项目排序无关紧要(它们可以是集合)。
  2.  'ABC' 中的元素的 len()==2 的所有组合,包括重复元素的组合。
  3. 'ABC'中的元素的len()==2的所有排列;生成的元组中的元素顺序是相关的。
  4.  'ABC' 和 'ABC' 生成的笛卡尔积(这是 repeat=2 的效果)。

我们将在本节中介绍的最后一组生成器函数旨在产生输入可迭代对象中的所有元素,但以某种方式重新排列。有两个函数返回多个生成器:itertools.groupby 和 itertools.tee。该组中的另一个生成器函数,reversed内置函数,是本节中介绍的唯一一个不接受任何可迭代对象作为而只接受序列的生成器函数作为参数的函数。这是有道理的:因为 reversed 将从最后一个到第一个向前产出元素,它仅适用于已知长度的序列。但是它通过根据需要产出每个元素,从而无需创建序列的反向副本。我将 itertools.product 函数与表 17-3 中的合并生成器放在一起,因为它们都接收了多个可迭代对象,而表 17-5 中的生成器都最多接收一个可迭代对象。

Table 17-5. Rearranging generator functions
ModuleFunctionDescription

itertools

groupby(it, key=None)

Yields 2-tuples of the form (key, group), where key is the grouping criterion and group is a generator yielding the items in the group

(built-in)

reversed(seq)

Yields items from seq in reverse order, from last to first; seq must be a sequence or implement the __reversed__ special method

itertools

tee(it, n=2)

Yields a tuple of n generators, each yielding the items of the input iterable independently

示例 17-21 演示了 itertools.groupby 和reversed内置函数的使用。 请注意, itertools.groupby 假设输入可迭代对象按分组标准排序,或者至少元素按该标准进行聚合——即使没有完全排序。评技术评论 Miroslav Šedivý 提出了这个用例:您可以按时间顺序对日期时间对象进行排序,然后按工作日分组以获取一组星期一数据,然后是星期二数据,等等,然后再按星期一(下周),依此类推.

示例 17-21. itertools.groupby

>>> list(itertools.groupby('LLLLAAGGG'))  1
[('L', <itertools._grouper object at 0x102227cc0>),
('A', <itertools._grouper object at 0x102227b38>),
('G', <itertools._grouper object at 0x102227b70>)]
>>> for char, group in itertools.groupby('LLLLAAAGG'):  2
...     print(char, '->', list(group))
...
L -> ['L', 'L', 'L', 'L']
A -> ['A', 'A',]
G -> ['G', 'G', 'G']
>>> animals = ['duck', 'eagle', 'rat', 'giraffe', 'bear',
...            'bat', 'dolphin', 'shark', 'lion']
>>> animals.sort(key=len)  3
>>> animals
['rat', 'bat', 'duck', 'bear', 'lion', 'eagle', 'shark',
'giraffe', 'dolphin']
>>> for length, group in itertools.groupby(animals, len):  4
...     print(length, '->', list(group))
...
3 -> ['rat', 'bat']
4 -> ['duck', 'bear', 'lion']
5 -> ['eagle', 'shark']
7 -> ['giraffe', 'dolphin']
>>> for length, group in itertools.groupby(reversed(animals), len): 5
...     print(length, '->', list(group))
...
7 -> ['dolphin', 'giraffe']
5 -> ['shark', 'eagle']
4 -> ['lion', 'bear', 'duck']
3 -> ['bat', 'rat']
>>>
  1. groupby 产出 (key, group_generator) 的元组。
  2. 处理 groupby 生成器需要嵌套迭代:在这种情况下,外部使用 for 循环和内部使用列表构造函数。
  3. 按长度对animals进行排序
  4. 同样,循环遍历key和group元组值对,以显示key并将group扩展为列表。
  5. 这里reversed生成器从右到左迭代animals。

该组中的最后一个生成器函数是 iterator.tee,它具有独特的行为:它从单个输入可迭代对象生成多个生成器,每个生成器从输入生成各个元素。这些生成器可以独立使用,如例 17-22 所示。

例 17-22。 itertools.tee 产出多个生成器,每个生成器生成输入的每一个元素

>>> list(itertools.tee('ABC'))
[<itertools._tee object at 0x10222abc8>, <itertools._tee object at 0x10222ac08>]
>>> g1, g2 = itertools.tee('ABC')
>>> next(g1)
'A'
>>> next(g2)
'A'
>>> next(g2)
'B'
>>> list(g1)
['B', 'C']
>>> list(g2)
['C']
>>> list(zip(*itertools.tee('ABC')))
[('A', 'A'), ('B', 'B'), ('C', 'C')]

请注意,本节中的几个示例使用了生成器函数的组合。这是这些函数的一大特点:因为它们将生成器作为参数并返回生成器,所以它们可以以多种不同的方式组合。

现在我们将回顾标准库中的另一组可迭代的函数。

可迭代的归约函数

表 17-6 中的函数都接受一个可迭代对象并返回一个结果。它们被称为“归约”、“合拢”或“累加”功能。我们可以实现这里列出的每一个内置函数 都可以通过functools.reduce实现,但它们作为内置函数存在是因为它们更容易处理一些常见的用例。关于 functools.reduce 的更多的介绍在“Vector Take #4: Hashing and a Faster ==”中。

对 all 和 any 来说,有一个重要的优化 functools.reduce 不支持:all 和 any 会短路——即一旦确定结果,它们就会停止使用迭代器。请参阅示例 17-23 中使用 any 的最后一个测试。

Table 17-6. Built-in functions that read iterables and return single values
ModuleFunctionDescription

(built-in)

all(it)

Returns True if all items in it are truthy, otherwise Falseall([]) returns True

(built-in)

any(it)

Returns True if any item in it is truthy, otherwise Falseany([]) returns False

(built-in)

max(it, [key=,] [default=])

Returns the maximum value of the items in it;a key is an ordering function, as in sorteddefault is returned if the iterable is empty

(built-in)

min(it, [key=,] [default=])

Returns the minimum value of the items in it.b key is an ordering function, as in sorteddefault is returned if the iterable is empty

functools

reduce(func, it, [initial])

Returns the result of applying func to the first pair of items, then to that result and the third item and so on; if given, initial forms the initial pair with the first item

(built-in)

sum(it, start=0)

The sum of all items in it, with the optional start value added (use math.fsum for better precision when adding floats)

a:也可以称为 max(arg1, arg2, …, [key=?]),在这种情况下,返回参数中的最大值。

b:也可以称为 min(arg1, arg2, …, [key=?]),在这种情况下,返回参数中的最小值。

示例 17-23 中演示了 all 和 any 的操作

例 17-23。某些序列的 all 和 any 的结果 

>>> all([1, 2, 3])
True
>>> all([1, 0, 3])
False
>>> all([])
True
>>> any([1, 2, 3])
True
>>> any([1, 0, 3])
True
>>> any([0, 0.0])
False
>>> any([])
False
>>> g = (n for n in [0, 0.0, 7, 8])
>>> any(g)  1
True
>>> next(g)  2
8
  1. 任何迭代 g 直到 g 产生 7;然后任何停止并返回 True
  2. 这就是为什么next(g)产生8的原因

另一个采用可迭代对象并返回其他内容的内置函数是sorted。与作为生成器函数的 reversed 不同,sorted 构建并返回新列表。毕竟,输入可迭代对象的每一个元素都必须被读取,以便对它们进行排序,并且排序发生在一个列表中,因此 sorted 只会在完成后返回该列表。我在这里提到 sorted 是因为它确实消耗了一个任意的可迭代对象。

当然, sorted 和 reduce 函数只适用于有限的迭代。否则,他们将继续收集元素并且永远不会返回结果。


NOTE:

如果你已经读到这里,你就已经看到了本章最重要和最有用的内容。其余部分涵盖了我们大多数人不常看到或不需要的高级生成器功能,例如yield from结构和经典协程。

还有一些关于类型提示迭代器、迭代器和经典协程的部分。


yield from 语法提供了一种组合生成器的新方法。接下来将进行介绍。

使用yield from的子生成器

Python 3.3 中引入了 yield from 表达式语法,以允许生成器将工作委托给子生成器。

在引入 yield from 之前,当生成器需要生成从另一个生成器生成的值时,我们使用了 for 循环:

>>> def sub_gen():
...     yield 1.1
...     yield 1.2
...
>>> def gen():
...     yield 1
...     for i in sub_gen():
...         yield i
...     yield 2
...
>>> for x in gen():
...     print(x)
...
1
1.1
1.2
2

我们可以使用 yield from 得到相同的结果:

例 17-24。测试驱动yield from。

>>> def sub_gen():
...     yield 1.1
...     yield 1.2
...
>>> def gen():
...     yield 1
...     yield from sub_gen()
...     yield 2
...
>>> for x in gen():
...     print(x)
...
1
1.1
1.2
2

在示例 17-24 中,for 循环是客户端代码,gen 是委托生成器,sub_gen 是子生成器。请注意,yield from 暂停了 gen,sub_gen 接管了代码的执行直到耗尽。sub_gen 产生的值通过 gen 直接传递给客户端 for 循环。同时, gen 被挂起,无法看到传给它的值。只有当 sub_gen 完成时,gen 才会恢复。

当子生成器包含带有值的 return 语句时,可以在委托生成器中使用 yield from 作为表达式的一部分来捕获该值。示例 17-25 进行了演示。

例 17-25。 yield from 获取子生成器的返回值。

>>> def sub_gen():
...     yield 1.1
...     yield 1.2
...     return 'Done!'
...
>>> def gen():
...     yield 1
...     result = yield from sub_gen()
...     print('<--', result)
...     yield 2
...
>>> for x in gen():
...     print(x)
...
1
1.1
1.2
<-- Done!
2

现在我们已经了解了 yield from 的基础知识,让我们研究几个简单但实​​用的例子来说明它的使用。

重新实现chain

我们在表 17-3 中看到 itertools 提供了一个chain生成器,它从多个可迭代对象中生成项目,首先迭代第一个可迭代对象,然后是第二个,依此类推直到最后一个。这是使用Python 中嵌套 for 循环的chain的实现:

>>> def chain(*iterables):
...     for it in iterables:
...         for i in it:
...             yield i
...
>>> s = 'ABC'
>>> r = range(3)
>>> list(chain(s, r))
['A', 'B', 'C', 0, 1, 2]

上面的chain生成器通过在内部 for 循环中驱动每个 it ,委托给每个可迭代的it。该内部循环可以用 yield from 表达式替换,如控制台的代码清单所示:

>>> def chain(*iterables):
...     for i in iterables:
...         yield from i
...
>>> list(chain(s, t))
['A', 'B', 'C', 0, 1, 2]

在这个例子中使用 yield from 是正确的,代码读起来更好,但它看起来像语法糖,几乎没有真正的收益。现在让我们开发一个更有趣的例子。

遍历一颗树

在本节中,我们将在脚本中看到 yield from 来遍历树结构。我将逐步构建它。

此示例的树结构是 Python 的异常层次结构。但是该模式可以很容易地进行调整以显示目录树或任何其他树结构。

从零级的 BaseException 开始,异常层次结构从 Python 3.10 开始深度为5级。我们的第一步是显示第0级。

给定一个根类,示例 17-26 中的树生成器生成它的名称并停止:

例 17-26。 tree/step0/tree.py:生成根类的名称并停止。

def tree(cls):
    yield cls.__name__


def display(cls):
    for cls_name in tree(cls):
        print(cls_name)


if __name__ == '__main__':
    display(BaseException)

示例 17-26 的输出只有一行:

BaseException

下一步将生成第 1 级。树生成器将生成根类的名称和每个直接子类的名称。子类的名称缩进以显示层次结构。这是我们想要的输出: 

$ python3 tree.py
BaseException
    Exception
    GeneratorExit
    SystemExit
    KeyboardInterrupt

示例 17-27 产生该输出的代码。

def tree(cls):
    yield cls.__name__, 0                        1
    for sub_cls in cls.__subclasses__():         2
        yield sub_cls.__name__, 1                3


def display(cls):
    for cls_name, level in tree(cls):
        indent = ' ' * 4 * level                 4
        print(f'{indent}{cls_name}')


if __name__ == '__main__':
    display(BaseException)
  1. 要支持缩进输出,需要生成类的名称及其在层次结构中的级别。
  2. 使用 __subclasses__ 特殊方法获取子类列表
  3. 生成子类名称和级别 1 。
  4. 构建 4 个空格乘以级别的缩进字符串。在零级,这将是一个空字符串。

在示例 17-28 中,我重构了tree以将根类的特殊情况与子类分开,子类现在在 sub_tree 生成器中处理。在 yield from 时,tree生成器被挂起并且 sub_tree 接管生成值。

def tree(cls):
    yield cls.__name__, 0
    yield from sub_tree(cls)              1


def sub_tree(cls):
    for sub_cls in cls.__subclasses__():
        yield sub_cls.__name__, 1         2


def display(cls):
    for cls_name, level in tree(cls):     3
        indent = ' ' * 4 * level
        print(f'{indent}{cls_name}')


if __name__ == '__main__':
    display(BaseException)
  1. 委托给 sub_tree 以生成子类的名称。
  2. 生成每个子类的名称和级别 1 。由于从tree内部的 sub_tree(cls) 生成,这些值完全绕过了tree生成器函数……
  3. ... 并直接在此处接收。

为了与婴儿步骤方法保持一致,我将编写我能想象到的最简单的代码来完成第 2 级。对于深度优先树遍历,在生成第 1 级中的每个节点之后,我想生成第 2 级中该节点的子节点,然后再恢复第 1 级。嵌套的 for 循环会处理这个问题,如示例 17-29 所示。

例 17-29。 tree/step3/tree.py:sub_tree 深度优先遍历第 1 级和第 2 级。

def tree(cls):
    yield cls.__name__, 0
    yield from sub_tree(cls)


def sub_tree(cls):
    for sub_cls in cls.__subclasses__():
        yield sub_cls.__name__, 1
        for sub_sub_cls in sub_cls.__subclasses__():
            yield sub_sub_cls.__name__, 2


def display(cls):
    for cls_name, level in tree(cls):
        indent = ' ' * 4 * level
        print(f'{indent}{cls_name}')


if __name__ == '__main__':
    display(BaseException)

这是运行示例 17-29 中的 step3/tree.py 的结果:

$ python3 tree.py
BaseException
    Exception
        TypeError
        StopAsyncIteration
        StopIteration
        ImportError
        OSError
        EOFError
        RuntimeError
        NameError
        AttributeError
        SyntaxError
        LookupError
        ValueError
        AssertionError
        ArithmeticError
        SystemError
        ReferenceError
        MemoryError
        BufferError
        Warning
    GeneratorExit
    SystemExit
    KeyboardInterrupt

你可能已经知道这是怎么回事,但我会再坚持一次:让我们通过添加另一个嵌套的 for 循环来达到第 3 级。程序的其余部分没有变化,因此示例 17-30 仅显示了 sub_tree 生成器。

例 17-30。来自 tree/step4/tree.py 的 sub_tree 生成器。

def sub_tree(cls):
    for sub_cls in cls.__subclasses__():
        yield sub_cls.__name__, 1
        for sub_sub_cls in sub_cls.__subclasses__():
            yield sub_sub_cls.__name__, 2
            for sub_sub_sub_cls in sub_sub_cls.__subclasses__():
                yield sub_sub_sub_cls.__name__, 3

示例 17-30 中有一个清晰的模式。我们执行一个 for 循环来获取级别 N 的子类。每次循环我们都会生成一个级别 N 的子类,然后启动另一个 for 循环来访问级别 N+1的子类。

在“重新发明chain”中,我们看到了如何用来自同一个生成器的 yield 来替换驱动生成器的嵌套 for 循环。我们可以在这里应用这个想法,如果我们让 sub_tree 接受一个level参数,并递归地从中产生,将当前子类作为具有下一个级别编号的新根类传递。请参见示例 17-31。

例 17-31。 tree/step5/tree.py:递归 sub_tree 在内存允许的范围内进行。

def tree(cls):
    yield cls.__name__, 0
    yield from sub_tree(cls, 1)


def sub_tree(cls, level):
    for sub_cls in cls.__subclasses__():
        yield sub_cls.__name__, level
        yield from sub_tree(sub_cls, level+1)


def display(cls):
    for cls_name, level in tree(cls):
        indent = ' ' * 4 * level
        print(f'{indent}{cls_name}')


if __name__ == '__main__':
    display(BaseException)

示例 17-31 可以遍历任意深度的树,仅受 Python 递归限制的限制。默认限制允许 1000 个待处理函数。

任何关于递归的优秀教程都会强调使用基本情况来避免无限递归的重要性。基本情况是不进行递归调用就返回的条件分支。基本情况通常用 if 语句实现。基本情况通常用 if 语句实现。在示例 17-31 中,sub_tree 没有 if,但在 for 循环中有一个隐式条件: 如果cls.__subclasses__()返回空列表,不执行循环体,因此不会发生递归调用。基本情况是 cls 类没有子类。在这种情况下, sub_tree 不会产生任何结果。它只是返回。

示例 17-31 按预期工作,但我们可以通过回忆我们达到第 3 级时观察到的模式(示例 17-30)使其更加简洁:我们生成一个级别为 N 的子类,然后开始一个嵌套的 for 循环来访问级别 N+1。在示例 17-31 中,我们用 yield from 替换了嵌套循环。现在我们可以将 tree 和 sub_tree 合并为一个生成器。示例 17-32 是此示例的最后一步。

例 17-32。 tree/step6/tree.py:tree的递归调用传递一个递增的level参数。

def tree(cls, level=0):
    yield cls.__name__, level
    for sub_cls in cls.__subclasses__():
        yield from tree(sub_cls, level+1)


def display(cls):
    for cls_name, level in tree(cls):
        indent = ' ' * 4 * level
        print(f'{indent}{cls_name}')


if __name__ == '__main__':
    display(BaseException)

在“带有 yield from 的子生成器”的开头,我们看到了 yield from 如何将子生成器直接连接到客户端代码,绕过委托生成器。当生成器用作协程时,这种连接变得非常重要,不仅从客户端代码生成值,并且消费这个值,正如我们将在“经典协程”中看到的那样。

在第一次遇到 yield from 之后,让我们转向类型提示迭代器和迭代器。

泛型可迭代类型

Python 的标准库有许多接受可迭代对象作为参数的函数。在您的代码中,可以像示例 8-15 中看到的 zip_replace 函数一样使用 collections.abc.Iterable 对此类函数进行注解(如果您必须支持 Python 3.8 或更早版本,则可以使用 Typing.Iterable,如“Legacy Support and Deprecated Collection Types”中所述):

例 17-33。 replacer.py 返回字符串元组的迭代器。

from collections.abc import Iterable

FromTo = tuple[str, str]  1

def zip_replace(text: str, changes: Iterable[FromTo]) -> str:  2
    for from_, to in changes:
        text = text.replace(from_, to)
    return text
  1. 定义类型别名;不是必需的,但使下一个类型提示更具可读性。从 Python 3.10 开始,FromTo 应该有一个 Typing.TypeAlias 的类型提示来阐明这一行这样写的原因:FromTo: TypeAlias = tuple[str, str]
  2. 注解changes使其接受 FromTo 元组的 Iterable。

Iterator类型不像 Iterable 类型那样经常出现,但它们也很容易编写。这是熟悉的斐波那契生成器,注解如下:

例 17-34。 fibo_gen.py: fibonacci 返回一个整数类型的生成器。

from collections.abc import Iterator

def fibonacci() -> Iterator[int]:
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

请注意,类型 Iterator 用于编码为带有 yield 的函数的生成器以及“手动”编写为带有 __next__ 的类的迭代器。还有一个 collections.abc.Generator 类型(以及相应的已弃用的typing.Generator),我们可以用它来注解生成器对象,但对于用作迭代器的生成器来说,它冗余的。

以下示例在使用 Mypy 进行检查时显示,Iterator 类型实际上是 Generator 类型的简化特例:

例 17-35。 iter_gen_type.py:两种注解迭代器的方法。

from collections.abc import Iterator
from keyword import kwlist
from typing import TYPE_CHECKING

short_kw = (k for k in kwlist if len(k) < 5)  1

if TYPE_CHECKING:
    reveal_type(short_kw)  2

long_kw: Iterator[str] = (k for k in kwlist if len(k) >= 4)  3

if TYPE_CHECKING:  4
    reveal_type(long_kw)
  1. 生成少于 5 个字符的 Python 关键字的生成器表达式。
  2. Mypy 推断:typing.Generator[builtins.str*, None, None]
  3. long_kw也会产生字符串,但我添加了一个显示的类型提示。
  4. 推断类型为: typing.Iterator[builtins.str].

abc.Iterator[str] 与 abc.Generator[str, None, None] 一致,因此 Mypy 在类型检查示例 17-35 中没有错误。

Iterator[T] 是 Generator[T, None, None] 的简写。两个注解都表示“生成 T 类型元素的生成器,但不消耗生成器或返回值。”能够消费和返回值的生成器是协程,我们的下一个主题。

经典协程
 


Note:

PEP 342—Coroutines via Enhanced Generators,引入了 .send() 和其他特性,可以将生成器用作协程。 PEP 342 使用的“协程”一词与我在这里使用的含义相同。

不幸的是,Python 的官方文档和标准库现在使用不一致的术语来指代用作协程的生成器,迫使我采用“经典协程”修饰语与较新的“原生协程”对象形成对比。

Python 3.5 出来后,趋势是使用“coroutine”作为“native coroutine”的同义词。但是 PEP 342 并没有被弃用,经典协程仍然按照最初的设计工作,尽管 asyncio 不再支持它们。


理解 Python 中的经典协程令人困惑,因为它们实际上是以不同方式使用的生成器。因此,让我们退后一步,考虑 Python 的另一个可以通过两种方式使用的特性。

我们在“元组不仅仅是不可变列表”中看到,我们可以将元组实例用作记录或不可变序列。当用作记录时,元组应具有特定数量的元素,并且每个元素可能具有不同的类型。当用作不可变列表时,元组可以具有任意长度,并且所有元素都应该具有相同的类型。这就是为什么有两种不同注解元素的类型提示的方法。

# A city record with name, country, and population:
city: tuple[str, str, int]

# An immutable sequence of domain names:
domains: tuple[str, ...]

类似的事情发生在generator上。它们通常用作迭代器,但它们也可以用作协程。“协程”实际上是在其主体中使用 yield 关键字创建的生成器函数。“协程对象”在物理结构上就是一个生成器对象。尽管在 C 中共享相同的底层实现,但 Python 中生成器和协程的用例是如此不同,以至于有两种不同的类型提示方式:

# The `readings` variable can be bound to an iterator
# or generator object that yields `float` items:
readings: Iterator[float]

# The `sim_taxi` variable can be bound to a coroutine
# representing a taxi cab in a discrete event simulation.
# It yields events, receives `float` timestamps, and returns
# the number of trips made during the simulation:
sim_taxi: Generator[Event, float, int]

更令人困惑的是,typing模块作者决定将该类型命名为 Generator,而实际上它描述了旨在用作协程的生成器对象的 API,而生成器更常用作简单的迭代器。

类型文档描述了 Generator 的形式类型参数,如下所示:

Generator[YieldType, SendType, ReturnType]

SendType 仅在生成器用作协程时才相关。该类型参数是调用 gen.send(x) 中 x 的类型。在编码为迭代器而不是协程的生成器上调用 .send() 是错误的。同样, ReturnType 仅对注解协程有意义,因为迭代器不像常规函数那样返回一个值。对用作迭代器的生成器唯一合理的操作是通过 for 循环和其他形式的迭代直接或间接调用 next(it)。YieldType 是调用 next(it) 返回的值的类型。

Generator 类型具有与 Typing.Coroutine 相同的类型参数:

Coroutine[YieldType, SendType, ReturnType]

Typing.Coroutine 文档实际上说“类型变量的方差和顺序对应于 Generator 的方差和顺序。”但是 Typing.Coroutine(已弃用)和 collections.abc.Coroutine(自 Python 3.9 起通用)旨在仅注释原生协程,而不是经典协程。如果你想在经典协程中使用类型提示,你会因为将它们注解为 Generator[YieldType, SendType, ReturnType] 而感到困惑。

David Beazley 创建了一些关于经典协程的最佳演讲和最全面的研讨会。在他的 PyCon 2009 course handout中,他有一张标题为“保持直线”的幻灯片,内容如下:

  • 生成器为迭代生成数据
  • 协程是数据的消费者
  • 为了防止你的大脑爆炸,不要把这两个概念混在一起
  • 协程与迭代无关
  • 注意:在协程中使用 yield 产生一个值,但它与迭代无关

                        ------David Beazley,关于协程和并发的好奇课程

示例:计算运行时平均值的协程

在第 9 章讨论闭包时,我们研究了对象来计算运行平均值:示例 9-7 展示了一个类,示例 9-13 展示了一个高阶函数,该函数返回一个函数,该函数在闭包中的调用之间保持 total 和 count 变量。示例 17-36 展示了如何使用协程执行相同的操作。

例 17-36。 coroaverager.py:计算运行时平均值的协程

from collections.abc import Generator

def averager() -> Generator[float, float, None]:  1
    total = 0.0
    count = 0
    average = 0.0
    while True:  2
        term = yield average  3
        total += term
        count += 1
        average = total/count
  1. 此函数返回一个生成浮点值的生成器,通过 .send() 接受浮点值,并且不返回有用的值
  2. 这个无限循环意味着只要客户端代码发送值,协程就会继续生成平均值。
  3. 此处的 yield 语句暂停协程,将结果生成给客户端,然后获取调用者发送给协程的值,开始无限循环的另一次迭代。

在协程中,total 和 count 可以是局部变量:在协程暂停等待下一个 .send() 时,不需要实例属性或闭包来保持上下文。这就是为什么协程是异步编程中回调的有吸引力的替代品:它们在激活期间能够保持本地状态。

示例 17-37 是 doctests,用于显示运行时的averager协程。

例 17-37。 coroaverager.py:示例 17-36 中运行averager协程的 doctest

  >>> coro_avg = averager()  1
    >>> next(coro_avg)  2
    0.0
    >>> coro_avg.send(10)  3
    10.0
    >>> coro_avg.send(30)
    20.0
    >>> coro_avg.send(5)
    15.0
  1. 创建协程对象。
  2. 启动协程。这产生了平均值的初始值:0.0。
  3. 现在开始工作了:每次调用 .send() 都会产生当前的平均值。

在示例 17-37 中,调用 next(coro_avg) 使协程前进到 yield,生成average的初始值。你也可以通过调用 coro_avg.send(None) 来启动协程——这实际上是 next() 内置函数所做的。但是你不能发送除 None 之外的任何值,因为协程只能在它被暂停在有yiled的行时才能接收一个发送的值。调用 next() 或 .send(None) 以前进到第一个 yield 被称为“预激协程”。每次激活后,协程恰好在 yield 关键字处暂停,等待发送值。行 coro_avg.send(10) 提供该值,导致协程激活。yield 表达式解析这个值为值 10,将其赋值给 term 变量。循环的其余部分更新total、count和average变量。while 循环中的下一次迭代产生average,协程再次暂停在 yield 关键字处。细心的读者可能很想知道averager实例(例如,coro_avg)的执行是如何终止的,因为它的主体是一个无限循环。我们通常不需要终止生成器,因为一旦没有有效引用,它就会被当做垃圾回收。如果您需要显示终止它,请使用 .close() 方法,如下所示:

例 17-38。 coroaverager.py:继续示例 17-37 

    >>> coro_avg.send(20)  1
    16.25
    >>> coro_avg.close()  2
    >>> coro_avg.close()  3
    >>> coro_avg.send(5)  4
    Traceback (most recent call last):
      ...
    StopIteration
  1. coro_avg 是示例 17-37 中创建的实例。
  2. .close() 方法在挂起的 yield 表达式处抛出 GeneratorExit异常。如果没有在协程函数中处理,异常将终止它。 GeneratorExit 被封装协程的生成器对象捕获——这就是我们看不到它的原因。
  3. 在先前关闭的协程上调用 .close() 无效
  4. 在关闭的协程上尝试 .send() 会抛出 StopIteration异常。

除了 .send() 方法,PEP 342—Coroutines via Enhanced Generators 还引入了一种让协程返回值的方法。下一节将展示如何操作。

从协程中返回结果

我们现在将研究另一个协程来计算平均值。此版本不会产生部分结果。相反,它返回一个包含项数和平均值的元组。我将清单分为两部分:示例 17-39 和示例 17-40。

例 17-39。 coroaverager2.py:代码的第一部分

from collections.abc import Generator
from typing import Union, NamedTuple

class Result(NamedTuple):  1
    count: int  # type: ignore  2
    average: float

class Sentinel:  3
    def __repr__(self):
        return f'<Sentinel>'

STOP = Sentinel()  4

SendType = Union[float, Sentinel]  5
  1. Example 17-40 中的 averager2 协程将返回一个 Result 实例。
  2. Result 实际上是 tuple 的一个子类,但是 .count() 方法并不是Result所需要的。 # type: ignore 注释防止 Mypy 抱怨有一个计数字段

  3. 使用可读的 __repr__ 生成哨兵值的类。

  4. 我将用于使协程停止收集数据并返回结果的哨兵值。

  5. 我将使用这个类型别名作为协程Generator返回类型的第二个类型参数,即发送类型参数。

上面的 SendType 定义在 Python 3.10 中也有效,但是如果你不需要支持早期版本,下面的写法会更好,要先从typing模块中导入 TypeAlias :

SendType: TypeAlias =  float | Sentinel

使用 |代替 Typing.Union 非常简洁并且可读性好,我可能不再需要创建那个类型别名,而是会像这样编写averager2的签名:

def averager2(verbose: bool = False) -> Generator[None, float | Sentinel, Result]:

现在让我们研究协程代码本身:

例 17-40。 coroaverager2.py:一个返回Result的协程

def averager2(verbose: bool = False) -> Generator[None, SendType, Result]:  1
    total = 0.0
    count = 0
    average = 0.0
    while True:
        term = yield  2
        if verbose:
            print('received:', term)
        if isinstance(term, Sentinel):  3
            break
        total += term  4
        count += 1
        average = total / count
    return Result(count, average)  5
  1. 对于这个协程,yield 类型是 None 因为它不产生数据。它接收 SendType 的数据并在完成后返回一个 Result 元组。
  2. 像这样使用 yield 仅在旨在消费数据的协程中才有意义。这将生成 None,但从 .send(term) 接收一个term。
  3. 如果term是Sentinel,则中断循环。由于这个 isinstance 检查…
  4. ...Mypy 允许我将term自增到total中,而不会标记错误,即我无法向可能是float或Sentinel的对象与float相加。
  5. 仅当 Sentinel 被发送到协程时,才会到达这一行。

现在让我们看看如何使用这个协程,从一个实际上不会产生结果的简单例子开始。

例 17-41。 coroaverager2.py:doctest 显示 .cancel()。

    >>> coro_avg = averager2()
    >>> next(coro_avg)
    >>> coro_avg.send(10)  1
    >>> coro_avg.send(30)
    >>> coro_avg.send(6.5)
    >>> coro_avg.close()  2
  1. 回想一下averager2 不会生成部分结果。它生成 None,Python 的控制台会忽略None。
  2. 在此协程中调用 .close() 使协程停止但不返回result,因为在协程中的 yield 行抛出 GeneratorExit 异常而中断执行,因此永远不会到达 return 语句。

现在让我们让它工作:

例 17-42。 coroaverager2.py:doctest 显示带有Result的 StopIteration。

    >>> coro_avg = averager2()
    >>> next(coro_avg)
    >>> coro_avg.send(10)
    >>> coro_avg.send(30)
    >>> coro_avg.send(6.5)
    >>> try:
    ...     coro_avg.send(STOP)  1
    ... except StopIteration as exc:
    ...     result = exc.value  2
    ...
    >>> result  3
    Result(count=3, average=15.5)
  1. 发送 STOP 哨兵会使协程从循环中中断并返回一个Result。包装协程的生成器对象随后抛出 StopIteration。
  2. StopIteration 实例有一个 value 属性绑定到终止协程的 return 语句的值。
  3. 信不信由你!这个就是结果

这种从包含在 StopIteration 异常中的协程中“走私”返回值的想法是一种奇怪的技巧。尽管如此,这个奇怪的 hack 是 PEP 342-Coroutines via Enhanced Generators 的一部分,并记录在 StopIteration 异常中,以及 Python 语言参考第 6 章的 Yield 表达式部分。

委托生成器可以直接使用 yield from 语法获取协程的返回值,如下所示。

例 17-43。 coroaverager2.py:doctest 显示带有Result的 StopIteration。

    >>> def compute():
    ...     res = yield from averager2(True)  1
    ...     print('computed:', res)  2
    ...     return res  3
    ...
    >>> comp = compute()  4
    >>> for v in [None, 10, 20, 30, STOP]:  5
    ...     try:
    ...         comp.send(v)  6
    ...     except StopIteration as exc:  7
    ...         result = exc.value
    received: 10
    received: 20
    received: 30
    received: <Sentinel>
    computed: Result(count=3, average=20.0)
    >>> result  8
    Result(count=3, average=20.0)
  1. res 会收集averager2的返回值; yield from的机制是在处理标志着协程终止的 StopIteration 异常时会检索返回值。
  2. 当为 verbose=True 时,verbose 参数使协程打印接收到的值,使其操作可见。
  3. 返回res。这也将包含在 StopIteration 中。
  4. 创建委托协程对象。
  5. 这个循环将驱动委托协程。
  6. 发送的第一个值是 None,以预激协程;最后一个是哨兵对象来停止协程。
  7. 捕获 StopIteration 异常以获取Compute的返回值
  8. 在averager2输出的行和compute之后,我们得到了一个Result实例。

尽管这里的示例没有做太多事情,但代码很难理解。使用 .send() 调用驱动协程并检索结果很复杂,除了 yield from--但我们只能在委托生成器/协程中使用该语法,它最终必须由一些non-trivial的代码驱动,如例 17-43 所示。

前面的例子表明,直接使用协程是繁琐和混乱的。添加异常处理和协程 .throw() 方法,示例会变得更加复杂。我不会在本书中介绍 .throw() 因为——就像 .send()——它只对“手动”驱动协程有用,但我不建议这样做,除非你基于协程的从头开始创造框架。
NOTE:

如果您对更深入地了解经典协程(包括 .throw() 方法)感兴趣,请查看 fluentpython.com 配套网站上的Classic Coroutines。这篇文章包括类似 Python 的伪代码,详细说明了如何驱动生成器和协程的收益,以及一个小型离散事件模拟,演示了一种使用协程的并发形式,过程没有使用异步编程框架。

在实践中,协程程序的高效工作需要专门框架的支持。这就是 asyncio 在 Python 3.3 中为经典协程提供的。随着 Python 3.5 中原生协程的出现,Python 核心开发人员正在逐步淘汰对 asyncio 中经典协程的支持。但底层机制非常相似。 async def 语法使原生协程更容易在代码中被识别,这是一个很大的优点。并且,原生协程使用 await 而不是 yield from 来委托给其他协程。第 21 章介绍了这部分。

现在让我们用一个关于协程类型提示中的协变和逆变的令人费解的部分来结束本章。

经典协程的泛型类型提示

回到“逆变类型”,我提到了 Typing.Generator 作为少数具有逆变类型参数的标准库类型之一。现在我们已经学习了经典的协程,我们已经准备好理解这种泛型类型了。

以下是在 Python 3.6 的 Typing.py 模块中声明 Typing.Generator 的方式:

T_co = TypeVar('T_co', covariant=True)
V_co = TypeVar('V_co', covariant=True)
T_contra = TypeVar('T_contra', contravariant=True)

# many lines omitted

class Generator(Iterator[T_co], Generic[T_co, T_contra, V_co],
                extra=_G_base):

泛型类型声明意味着 Generator 类型提示需要我们之前见过的三个类型参数:

my_coro : Generator[YieldType, SendType, ReturnType]

从形参中的类型变量可以看出 YieldType 和 ReturnType 是协变的,而 SendType 是逆变的。要了解原因,请考虑 YieldType 和 ReturnType 是“输出”类型。两者都描述了来自协程对象的数据——即用作协程对象时的生成器对象。

这些是协变是有道理的,因为任何期望产生浮点数的协程的代码都可以使用产生整数的协程。这就是为什么 Generator 在它的 YieldType 参数上是协变的。相同的推理适用于 ReturnType 参数 - 也是协变的。

使用“协变类型”中介绍的符号,第一个和第三个参数的协方差由指向同一方向的 :> 符号表示:

   float :> int
Generator[float, Any, float] :> Generator[int, Any, int]

YieldType 和 ReturnType 是“逆变的经验规则”的第一条规则的示例:

1如果形式类型参数为来自对象的数据定义了类型,则它可以是协变的。

另一方面,SendType 是一个“输入”参数:它是协程对象的 .send(value) 方法的 value 参数的类型。需要向协程发送浮点数的客户端代码不能使用 SendType为int类型 的协程,因为 float 不是 int 的子类型。换句话说,float 与 int 不一致。但是客户端可以使用complex作为SendType的协程,因为float是complex的子类型,所以float与complex是一致的。

:> 符号可以看到第二个参数的逆变:

                     float :> int
Generator[Any, float, Any] <: Generator[Any, int, Any]

这是第二个经验差异规则的示例:

2如果形式类型参数为初始构造后进入对象的数据定义类型,则它可以是逆变的。

这个关于方差的愉快讨论完成了本书最长的一章。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值