流畅的 Python 第二版(GPT 重译)(九)(1)

简介: 流畅的 Python 第二版(GPT 重译)(九)

第四部分:控制流

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

当我在我的程序中看到模式时,我认为这是一个麻烦的迹象。程序的形状应该只反映它需要解决的问题。代码中的任何其他规律性对我来说都是一个迹象,至少对我来说,这表明我使用的抽象不够强大——通常是我手动生成我需要编写的某个宏的扩展。

Paul Graham,Lisp 程序员和风险投资家¹

迭代对于数据处理是基础的:程序将计算应用于数据系列,从像素到核苷酸。如果数据不适合内存,我们需要惰性地获取项目——一次一个,并按需获取。这就是迭代器的作用。本章展示了迭代器设计模式是如何内置到 Python 语言中的,因此您永远不需要手动编写它。

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

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

本章涵盖以下主题:

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

本章的新内容

“使用 yield from 的子生成器” 从一页发展到六页。现在它包括了演示使用yield from生成器行为的更简单实验,以及逐步开发树数据结构遍历的示例。

新的部分解释了IterableIteratorGenerator类型的类型提示。

本章的最后一个重要部分,“经典协程”,是对一个主题的介绍,第一版中占据了一个 40 页的章节。我更新并将“经典协程”章节移至伴随网站的帖子,因为这是读者最具挑战性的章节,但在 Python 3.5 引入原生协程后,其主题的相关性较小,我们将在第二十一章中学习。

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

一系列单词

我们将通过实现一个Sentence类来开始探索可迭代对象:你可以将一些文本传递给它的构造函数,然后逐个单词进行迭代。第一个版本将实现序列协议,并且它是可迭代的,因为所有序列都是可迭代的——正如我们在第一章中所看到的。现在我们将看到确切的原因。

示例 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)  # ①
    def __getitem__(self, index):
        return self.words[index]  # ②
    def __len__(self):  # ③
        return len(self.words)
    def __repr__(self):
        return 'Sentence(%s)' % reprlib.repr(self.text)  # ④

.findall 返回一个字符串列表,其中包含正则表达式的所有非重叠匹配。

self.words保存了.findall的结果,因此我们只需返回给定索引处的单词。

为了完成序列协议,我们实现了__len__,尽管不需要使其可迭代。

reprlib.repr是一个实用函数,用于生成数据结构的缩写字符串表示,这些数据结构可能非常庞大。²

默认情况下,reprlib.repr将生成的字符串限制为 30 个字符。查看示例 17-2 中的控制台会话,了解如何使用Sentence

示例 17-2。在Sentence实例上测试迭代
>>> s = Sentence('"The time has come," the Walrus said,')  # ①
>>> s
Sentence('"The time ha... Walrus said,') # ②
>>> for word in s:  # ③
...     print(word)
The time has come the Walrus said >>> list(s)  # ④
['The', 'time', 'has', 'come', 'the', 'Walrus', 'said']

从字符串创建一个句子。

注意使用reprlib.repr生成的__repr__输出中的...

Sentence实例是可迭代的;我们马上就会看到原因。

作为可迭代对象,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'对象不可迭代,其中C是目标对象的类。

这就是为什么所有的 Python 序列都是可迭代的:根据定义,它们都实现了__getitem__。事实上,标准序列也实现了__iter__,你的序列也应该实现,因为通过__getitem__进行迭代是为了向后兼容,可能在未来会被移除——尽管在 Python 3.10 中尚未被弃用,我怀疑它会被移除。

如“Python 挖掘序列”中所述,这是一种极端的鸭子类型:一个对象被视为可迭代对象不仅当它实现了特殊方法__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

在鹅类型方法中,可迭代对象的定义更简单但不够灵活:如果一个对象实现了__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
提示

截至 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块中这样做而不是进行显式检查。显式检查可能在稍后持有对象以进行迭代时是有意义的;在这种情况下,尽早捕获错误会使调试更容易。

iter()内置函数更常被 Python 自身使用,而不是我们自己的代码。我们可以用第二种方式使用它,但这并不是广为人知的。

使用可调用对象调用 iter

我们可以使用两个参数调用iter()来从函数或任何可调用对象创建迭代器。在这种用法中,第一个参数必须是一个可调用对象,以便重复调用(不带参数)以产生值,第二个参数是一个sentinel:一个标记值,当可调用对象返回该值时,迭代器会引发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赋值,这在原始示例中没有。partial()函数是必需的,因为传递给iter()的可调用对象不应该需要参数。在示例中,一个空的bytes对象是标记值,因为这就是f.read在没有更多字节可读时返回的值。

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

可迭代对象与迭代器

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

可迭代的

任何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)  # ①
>>> while True:
...     try:
...         print(next(it))  # ②
...     except StopIteration:  # ③
...         del it  # ④
...         break  # ⑤
...
A B C

从可迭代对象构建迭代器it

反复调用迭代器上的next以获取下一个项目。

当没有更多项目时,迭代器会引发StopIteration

释放对it的引用——迭代器对象被丢弃。

退出循环。

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

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

__next__

返回系列中的下一个项目,如果没有更多,则引发StopIteration

__iter__

返回self;这允许迭代器在期望可迭代对象的地方使用,例如在for循环中。

该接口在collections.abc.Iterator ABC 中得到规范化,它声明了__next__抽象方法,并且子类化Iterable——在那里声明了抽象的__iter__方法。参见图 17-1。

图 17-1。IterableIterator ABCs。斜体的方法是抽象的。一个具体的Iterable.__iter__应返回一个新的Iterator实例。一个具体的Iterator必须实现__next__Iterator.__iter__方法只返回实例本身。

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):  # ①
        if cls is Iterator:
            return _check_methods(C, '__iter__', '__next__')  # ②
        return NotImplemented

__subclasshook__支持使用isinstanceissubclass进行结构类型检查。我们在“使用 ABC 进行结构类型检查”中看到了它。

_check_methods 遍历类的__mro__以检查方法是否在其基类中实现。它在同一Lib/_collections_abc.py模块中定义。如果方法已实现,则C类将被识别为Iterator的虚拟子类。换句话说,issubclass(C, Iterable)将返回True

警告

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__subclasshook__方法就是这样做的。

提示

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

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

>>> s3 = Sentence('Life of Brian')  # ①
>>> it = iter(s3)  # ②
>>> it  # doctest: +ELLIPSIS
<iterator object at 0x...> >>> next(it)  # ③
'Life' >>> next(it)
'of' >>> next(it)
'Brian' >>> next(it)  # ④
Traceback (most recent call last):
  ...
StopIteration
>>> list(it)  # ⑤
[] >>> list(iter(s3))  # ⑥
['Life', 'of', 'Brian']

创建一个包含三个单词的句子s3

s3获取一个迭代器。

next(it) 获取下一个单词。

没有更多的单词了,所以迭代器会引发StopIteration异常。

一旦耗尽,迭代器将始终引发StopIteration,这使其看起来像是空的。

要再次遍历句子,必须构建一个新的迭代器。

因为迭代器所需的唯一方法是__next____iter__,所以没有办法检查是否还有剩余的项,除非调用next()并捕获StopIteration。此外,无法“重置”迭代器。如果需要重新开始,必须在第一次构建迭代器的可迭代对象上调用iter()。在迭代器本身上调用iter()也不会有帮助,因为正如前面提到的,Iterator.__iter__是通过返回self来实现的,因此这不会重置已耗尽的迭代器。

这种最小接口是合理的,因为实际上,并非所有迭代器都可以重置。例如,如果一个迭代器正在从网络中读取数据包,就无法倒带。³

来自 Example 17-1 的第一个Sentence版本之所以可迭代,是因为iter()内置函数对序列的特殊处理。接下来,我们将实现Sentence的变体,这些变体实现了__iter__以返回迭代器。

带有__iter__的句子类

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

句子接收 #2: 经典迭代器

下一个Sentence实现遵循设计模式书中经典迭代器设计模式的蓝图。请注意,这不是 Python 的惯用写法,因为接下来的重构将非常清楚地表明。但是,展示可迭代集合和与之一起使用的迭代器之间的区别是有用的。

Example 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):  # ①
        return SentenceIterator(self.words)  # ②
class SentenceIterator:
    def __init__(self, words):
        self.words = words  # ③
        self.index = 0  # ④
    def __next__(self):
        try:
            word = self.words[self.index]  # ⑤
        except IndexError:
            raise StopIteration()  # ⑥
        self.index += 1  # ⑦
        return word  # ⑧
    def __iter__(self):  # ⑨
        return self

__iter__方法是对先前Sentence实现的唯一补充。这个版本没有__getitem__,以明确表明该类之所以可迭代是因为它实现了__iter__

__iter__ 通过实例化并返回一个迭代器来实现可迭代协议。

SentenceIterator 持有对单词列表的引用。

self.index 确定下一个要获取的单词。

获取self.index处的单词。

如果在self.index处没有单词,则引发StopIteration

增加 self.index

返回单词。

实现 self.__iter__

Example 17-4 中的代码通过 Example 17-2 中的测试。

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

这是一项繁重的工作(对于我们这些被宠坏的 Python 程序员来说)。请注意,SentenceIterator 中的大部分代码都涉及管理迭代器的内部状态。很快我们将看到如何避免这种繁琐的工作。但首先,让我们简要地讨论一下可能会诱人但却是错误的实现快捷方式。

不要将可迭代对象作为自身的迭代器。

在构建可迭代对象和迭代器时常见的错误是混淆两者。明确一点:可迭代对象具有一个 __iter__ 方法,每次实例化一个新的迭代器。迭代器实现了一个返回单个项的 __next__ 方法,以及一个返回 self__iter__ 方法。

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

可能会诱人在 Sentence 类中实现 __next__ 以及 __iter__,使每个 Sentence 实例同时成为自身的可迭代器和迭代器。但这很少是一个好主意。根据在 Google 审查 Python 代码方面拥有丰富经验的 Alex Martelli 的说法,这也是一个常见的反模式。

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

使用迭代器模式

  • 访问聚合对象的内容而不暴露其内部表示。

  • 以支持聚合对象的多次遍历。

  • 为不同的聚合结构提供统一的遍历接口(即支持多态迭代)。

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

现在经典的迭代器模式已经得到了正确的演示,我们可以放手了。Python 从 Barbara Liskov 的 CLU 语言 中引入了 yield 关键字,因此我们不需要手动“生成”代码来实现迭代器。

接下来的章节将呈现更符合 Python 习惯的 Sentence 版本。

Sentence Take #3:生成器函数

同样功能的 Python 实现使用了生成器,避免了实现 SentenceIterator 类的所有工作。生成器的正确解释就在 Example 17-5 之后。

Example 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:  # ①
            yield word  # ②
        # ③
# done! # ④

遍历 self.words

产出当前的 word

明确的 return 不是必需的;函数可以“顺利执行”并自动返回。无论哪种方式,生成器函数不会引发 StopIteration:当完成生成值时,它只是退出。

不需要单独的迭代器类!

这里我们再次看到了一个不同的 Sentence 实现,通过了 Example 17-2 中的测试。

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

紧随其后是对生成器的全面解释。

生成器的工作原理

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

提示

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

示例 17-6 展示了一个简单生成器函数的行为。⁶

示例 17-6. 一个生成三个数字的生成器函数
>>> def gen_123():
...     yield 1  # ①
...     yield 2
...     yield 3
...
>>> gen_123  # doctest: +ELLIPSIS
<function gen_123 at 0x...>  # ②
>>> gen_123()   # doctest: +ELLIPSIS
<generator object gen_123 at 0x...>  # ③
>>> for i in gen_123():  # ④
...     print(i)
1
2
3
>>> g = gen_123()  # ⑤
>>> next(g)  # ⑥
1
>>> next(g)
2
>>> next(g)
3
>>> next(g)  # ⑦
Traceback (most recent call last):
  ...
StopIteration

生成器函数的函数体通常在循环中有yield,但不一定;这里我只是重复了三次yield

仔细观察,我们可以看到gen_123是一个函数对象。

但是当调用gen_123()时,会返回一个生成器对象。

生成器对象实现了Iterator接口,因此它们也是可迭代的。

我们将这个新的生成器对象赋给g,这样我们就可以对其进行实验。

因为g是一个迭代器,调用next(g)会获取yield产生的下一个项目。

当生成器函数返回时,生成器对象会引发StopIteration

生成器函数构建一个包装函数体的生成器对象。当我们在生成器对象上调用next()时,执行会前进到函数体中的下一个yield,而next()调用会评估在函数体暂停时产生的值。最后,由 Python 创建的封闭生成器对象在函数体返回时引发StopIteration,符合Iterator协议。

流畅的 Python 第二版(GPT 重译)(九)(2)https://developer.aliyun.com/article/1484705

相关文章
|
5天前
|
数据采集 存储 人工智能
【Python+微信】【企业微信开发入坑指北】4. 企业微信接入GPT,只需一个URL,自动获取文章总结
【Python+微信】【企业微信开发入坑指北】4. 企业微信接入GPT,只需一个URL,自动获取文章总结
43 0
|
5天前
|
Python
过年了,让GPT用Python给你写个放烟花的程序吧!
过年了,让GPT用Python给你写个放烟花的程序吧!
21 0
|
5天前
|
人工智能 JSON 机器人
【Chat GPT】用 ChatGPT 运行 Python
【Chat GPT】用 ChatGPT 运行 Python
|
5天前
|
机器学习/深度学习 人工智能 自然语言处理
总结几个GPT的超实用之处【附带Python案例】
总结几个GPT的超实用之处【附带Python案例】
|
5天前
|
前端开发 JavaScript 安全
JavaScript 权威指南第七版(GPT 重译)(七)(4)
JavaScript 权威指南第七版(GPT 重译)(七)
29 0
|
5天前
|
前端开发 JavaScript 算法
JavaScript 权威指南第七版(GPT 重译)(七)(3)
JavaScript 权威指南第七版(GPT 重译)(七)
38 0
|
5天前
|
前端开发 JavaScript Unix
JavaScript 权威指南第七版(GPT 重译)(七)(2)
JavaScript 权威指南第七版(GPT 重译)(七)
43 0
|
5天前
|
前端开发 JavaScript 算法
JavaScript 权威指南第七版(GPT 重译)(七)(1)
JavaScript 权威指南第七版(GPT 重译)(七)
69 0
|
5天前
|
存储 前端开发 JavaScript
JavaScript 权威指南第七版(GPT 重译)(六)(4)
JavaScript 权威指南第七版(GPT 重译)(六)
126 3
JavaScript 权威指南第七版(GPT 重译)(六)(4)
|
5天前
|
前端开发 JavaScript API
JavaScript 权威指南第七版(GPT 重译)(六)(3)
JavaScript 权威指南第七版(GPT 重译)(六)
68 4