第四部分:控制流
第十七章:迭代器、生成器和经典协程
当我在我的程序中看到模式时,我认为这是一个麻烦的迹象。程序的形状应该只反映它需要解决的问题。代码中的任何其他规律性对我来说都是一个迹象,至少对我来说,这表明我使用的抽象不够强大——通常是我手动生成我需要编写的某个宏的扩展。
Paul Graham,Lisp 程序员和风险投资家¹
迭代对于数据处理是基础的:程序将计算应用于数据系列,从像素到核苷酸。如果数据不适合内存,我们需要惰性地获取项目——一次一个,并按需获取。这就是迭代器的作用。本章展示了迭代器设计模式是如何内置到 Python 语言中的,因此您永远不需要手动编写它。
Python 中的每个标准集合都是可迭代的。可迭代是提供迭代器的对象,Python 使用它来支持诸如:
for
循环- 列表、字典和集合推导
- 解包赋值
- 集合实例的构建
本章涵盖以下主题:
- Python 如何使用
iter()
内置函数处理可迭代对象 - 如何在 Python 中实现经典迭代器模式
- 经典迭代器模式如何被生成器函数或生成器表达式替代
- 详细介绍生成器函数的工作原理,逐行描述
- 利用标准库中的通用生成器函数
- 使用
yield from
表达式组合生成器 - 为什么生成器和经典协程看起来相似但用法却截然不同,不应混合使用
本章的新内容
“使用 yield from 的子生成器” 从一页发展到六页。现在它包括了演示使用yield from
生成器行为的更简单实验,以及逐步开发树数据结构遍历的示例。
新的部分解释了Iterable
、Iterator
和Generator
类型的类型提示。
本章的最后一个重要部分,“经典协程”,是对一个主题的介绍,第一版中占据了一个 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
内置函数:
- 检查对象是否实现了
__iter__
,并调用它以获取迭代器。 - 如果未实现
__iter__
,但实现了__getitem__
,那么iter()
会创建一个迭代器,尝试从 0(零)开始按索引获取项目。 - 如果失败,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。Iterable
和Iterator
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__
支持使用isinstance
和issubclass
进行结构类型检查。我们在“使用 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