CHAPTER 17 Iterators, Generators, and Classic Coroutines
从17章到21章都是第四大章节 control flow的部分
一、介绍
迭代是数据处理的基础:程序对数据序列进行计算,这些数据序列涵盖从像素到核苷酸等各种类型。如果数据无法全部存入内存,我们就需要按需惰性获取数据——一次获取一个数据项。这正是迭代器的作用。本章将展示迭代器设计模式是如何内置于Python语言中的,这样你就无需手动编写相关代码。
Python中的每个标准集合都是可迭代的。可迭代对象是指能提供迭代器的对象,Python利用迭代器来支持以下操作:
- for循环
- 列表、字典和集合推导式
- 解包赋值
- 集合实例的创建
本章涵盖以下主题:
- Python如何使用内置的iter()函数处理可迭代对象
- 如何在Python中实现经典的迭代器模式
- 经典迭代器模式如何被生成器函数或生成器表达式替代
- 生成器函数的详细工作原理,包括逐行解析
- 如何利用标准库中的通用生成器函数
- 使用yield from表达式组合生成器
- 为什么生成器和经典协程看似相似,但使用方式却大相径庭,且不应混淆
二、A Sequence of Words
1、引言:可迭代对象与序列
在 Python 中,可迭代对象(iterable) 是指能够一次返回一个其成员的对象。常见的可迭代对象包括列表、元组、字符串、字典、集合等。而序列(sequence) 是一种更具体的可迭代对象,它支持通过整数索引进行元素访问,并且通常具有固定的长度。
理解可迭代对象和序列的概念对于编写高效、优雅的 Python 代码至关重要。本笔记将通过实现一个 Sentence
类,深入探讨可迭代对象和序列的实现方式,并结合实际例子进行详细讲解。
2、原始 Sentence
类解析
1. 类的功能
Sentence
类的目标是:
- 接受一个字符串作为输入,并将其拆分成单词列表。
- 支持迭代,可以逐个遍历单词。
- 支持通过索引访问单词,类似于列表。
2. 代码详解
import re
import reprlibRE_WORD = re.compile(r'\w+')class Sentence:def __init__(self, text):self.text = textself.words = RE_WORD.findall(text)def __getitem__(self, index):return self.words[index]def __len__(self):return len(self.words)def __repr__(self):return f'Sentence({reprlib.repr(self.text)})'
(1) 正则表达式与 re
模块
RE_WORD = re.compile(r'\w+')
:使用正则表达式\w+
匹配单词,re.compile
将其编译为正则表达式对象,提高匹配效率。\w
匹配字母、数字和下划线,相当于[a-zA-Z0-9_]
。+
表示匹配前面的模式一次或多次。
(2) __init__
方法
-
self.text = text
:存储原始文本。 -
self.words = RE_WORD.findall(text)
:使用findall
方法查找所有非重叠的匹配项,返回一个包含所有单词的列表。>>> s = Sentence("Hello, world!") >>> s.words ['Hello', 'world']
-
- 匹配过程:
- “Hello”:匹配
\w+
,因为 “Hello” 是连续的字母。 - “,”:不匹配
\w+
,因为逗号不是字母、数字或下划线。 - " world":匹配
\w+
,因为 “world” 是连续的字母。 - “!”:不匹配
\w+
,因为感叹号不是字母、数字或下划线。
-
- 结果:
- 因此,
re.findall
返回['Hello', 'world']
,因为只有这两个子字符串符合\w+
的模式。
(3) __getitem__
方法
-
__getitem__(self, index)
:实现通过索引访问元素的功能。return self.words[index]
:返回指定索引位置的单词。
>>> s = Sentence("Hello, world!") >>> s[0] 'Hello' >>> s[1] 'world' >>> s[-1] 'world'
注意: 实现了
__getitem__
方法后,Python 会自动使对象成为可迭代对象,因为迭代器会调用__getitem__
方法从 0 开始依次访问元素,直到引发IndexError
。
(4) __len__
方法
-
__len__(self)
:实现len()
函数的功能,返回单词列表的长度。>>> s = Sentence("Hello, world!") >>> len(s) 2
注意: 虽然实现了
__len__
方法可以让我们使用len()
函数,但并不是使对象成为可迭代对象的必要条件。
(5) __repr__
方法
__repr__(self)
:定义对象的“官方”字符串表示形式,用于调试和日志记录。- 使用
reprlib.repr
可以生成更简洁的字符串表示,避免输出过长的文本。
- 使用
s = Sentence('"The time has come," the Walrus said,')
print(s)
# Sentence('"The time ha... Walrus said,')
注意: reprlib.repr
默认将输出字符串长度限制在 30 个字符以内。
3、迭代过程详解
Sentence
对象之所以是可迭代的,是因为它实现了 __getitem__
方法。以下是迭代过程的详细步骤:
-
调用
iter(s)
:- Python 会调用
s.__iter__()
方法(如果存在)。 - 如果
__iter__
方法不存在,Python 会退而求其次,尝试调用s.__getitem__
方法,从索引 0 开始依次访问元素,直到引发StopIteration
异常。
- Python 会调用
-
迭代过程:
- 第一次调用
s.__getitem__(0)
,返回第一个单词。 - 第二次调用
s.__getitem__(1)
,返回第二个单词。 - 以此类推,直到所有单词都被访问完毕。
- 当索引超出范围时,Python 会引发
StopIteration
异常,结束迭代。
>>> s = Sentence("Hello, world!") >>> for word in s: ... print(word) Hello world
- 第一次调用
-
执行顺序
-
只要存在
__iter__和__next__方法
执行逻辑顺序:
实际迭代时遵循固定调用顺序:iter(obj)→obj.__iter__()→next(iterator)→iterator.__next__()
-
如果不存在
__iter__和__next__方法
当使用for循环或iter()函数时,Python首先查找__iter__方法
如果未找到__iter__,则尝试调用__getitem__方法,从索引0开始顺序访问元素
无法直接响应next()函数调用(必须通过iter()先创建迭代器)
4、对比:实现 __iter__
方法的 Sentence
类
虽然通过实现 __getitem__
方法可以使对象成为可迭代对象,但更常见、更高效的做法是实现 __iter__
方法。下面是一个实现 __iter__
方法的 Sentence
类示例:
class SentenceIter:def __init__(self, text):self.text = textself.words = RE_WORD.findall(text)def __iter__(self):return iter(self.words)def __repr__(self):return f'SentenceIter({reprlib.repr(self.text)})'
-
__iter__
方法:- 直接返回
self.words
的迭代器。 - 这样,迭代过程将直接由
self.words
的迭代器处理,效率更高。
>>> s = SentenceIter("Hello, world!") >>> for word in s: ... print(word) Hello world
- 直接返回
对比:
特性 | Sentence (实现 __getitem__ ) | SentenceIter (实现 __iter__ ) |
---|---|---|
迭代方式 | 通过索引访问元素 | 直接使用迭代器 |
效率 | 较低,因为每次迭代都需要调用 __getitem__ | 较高,因为直接使用迭代器 |
适用场景 | 适用于需要通过索引访问元素的场景 | 适用于大多数可迭代对象的场景 |
三、Why Sequences Are Iterable: The iter Function
1. 理解 Python 中序列的可迭代性
在 Python 中,可迭代性(iterability)是对象的一个重要特性,它使得对象能够被用于 for
循环、list()
、tuple()
等需要逐项处理数据的场景。
1.1 iter()
函数的工作机制
当 Python 需要迭代一个对象 x
时,会自动调用 iter(x)
。iter()
函数的工作机制如下:
-
检查
__iter__
方法:- 如果对象实现了
__iter__
方法,iter()
会调用该方法返回一个迭代器(iterator)。 - 迭代器必须实现
__next__
方法,用于逐个返回元素,并在没有更多元素时抛出StopIteration
异常。
示例:
- 如果对象实现了
class MyIterable:def __iter__(self):self.current = 0return selfdef __next__(self):if self.current < 3:self.current += 1return self.currentelse:raise StopIterationmy_iterable = MyIterable()
for item in my_iterable:print(item) # 输出: 1 2 3
-
回退到
__getitem__
方法:- 如果对象没有实现
__iter__
方法,但实现了__getitem__
方法,iter()
会创建一个迭代器,该迭代器会尝试通过索引(从0
开始)获取元素。 - 当索引超出范围时,
__getitem__
抛出IndexError
,迭代器将其解释为没有更多元素,并停止迭代。
示例:
- 如果对象没有实现
class MySequence:def __getitem__(self, index):if index < 3:return index * 2else:raise IndexErrormy_sequence = MySequence()
for item in my_sequence:print(item) # 输出: 0 2 4
-
无法迭代时抛出
TypeError
:- 如果对象既没有实现
__iter__
也没有实现__getitem__
,iter()
会抛出TypeError
,提示对象不可迭代。
示例:
- 如果对象既没有实现
class MyNonIterable:passmy_non_iterable = MyNonIterable()
for item in my_non_iterable:print(item)
Traceback (most recent call last):File "d:\AAAAAstudywork\sec8b\test\testt.py", line 5, in <module>for item in my_non_iterable:
TypeError: 'MyNonIterable' object is not iterable
1.2 序列的可迭代性
根据上述机制,所有 Python 序列(如 list
、tuple
、str
等)都是可迭代的,因为它们都实现了 __getitem__
方法。事实上,标准的序列类型也实现了 __iter__
方法,这是为了提供更高效的迭代方式。
注意:虽然 __getitem__
提供了可迭代性,但它主要是为了向后兼容,未来可能会被移除。因此,建议在自定义序列类型时同时实现 __iter__
和 __getitem__
方法。
2. 深入理解 iter()
与鸭子类型
2.1 鸭子类型(Duck Typing)
Python 采用的是鸭子类型,即“如果它走路像鸭子,叫声像鸭子,那么它就是鸭子”。在可迭代性的上下文中,这意味着:
- 不仅实现了
__iter__
方法的对象是可迭代的。 - 还实现了
__getitem__
方法的对象也被视为可迭代的。
示例:
class Spam:def __getitem__(self, i):print('->', i)if i < 2:return ielse:raise IndexErrorspam_can = Spam()
print(iter(spam_can))
print(list(spam_can))
<iterator object at 0x000000000138FF10>
-> 0
-> 1
-> 2
[0, 1]
iter(spam_can)
当调用iter(spam_can)
时,Python会尝试获取spam_can的迭代器对象。由于Spam类未实现__iter__
方法,解释器会转而检查__getitem__
方法,并自动创建一个内置的iterator对象。该迭代器会记录当前迭代状态(初始索引为0)。
为什么会返回[0, 1]
一步步拆解 list(spam_can)
的执行过程,看看临时列表是如何动态生成的
🔍 分步解析:临时列表的生成过程
1️⃣ list()
的底层行为
当调用 list(spam_can)
时,Python 会尝试将 spam_can
转换为列表。具体步骤如下:
- 检查对象是否可迭代:
- 首先查找
__iter__
方法(你的类没有实现,跳过) - 退而使用
__getitem__
协议(因为你的类实现了它)
- 首先查找
2️⃣ 迭代过程(临时列表动态填充)
Python 会隐式创建一个临时空列表,然后通过以下循环填充它:
_temp_list = [] # 这就是"临时列表"的初始状态
i = 0 # 从索引0开始尝试获取元素
while True:try:value = spam_can[i] # 调用 __getitem__(i)_temp_list.append(value) # 将返回值加入临时列表i += 1 # 索引递增except IndexError: # 遇到IndexError时终止break
3️⃣ 结合代码执行流程
步骤 | 操作 | 输出/效果 | 临时列表状态 |
---|---|---|---|
1 | spam_can[0] | 打印 -> 0 ,返回 0 | [0] |
2 | spam_can[1] | 打印 -> 1 ,返回 1 | [0, 1] |
3 | spam_can[2] | 打印 -> 2 ,抛出 IndexError | 循环终止 |
4️⃣ 最终结果
- 临时列表
_temp_list
最终为[0, 1]
- 这个值被赋给
result
变量,即result = [0, 1]
💡 关键结论
-
临时列表的来源:
- 是
list()
函数内部隐式创建的,用于暂存迭代过程中获取的元素。 - 它本质是一个动态增长的Python列表,通过
append()
逐步填充。
- 是
-
你的
__getitem__
如何驱动这个过程:- Python 自动从
i=0
开始调用,直到遇到IndexError
- 每次返回值都会被追加到临时列表
- Python 自动从
-
为什么是
[0, 1]
:- 因为
i=0
和i=1
时返回了有效值,而i=2
时主动抛出异常终止迭代
- 因为
isinstance
然而,需要注意的是,isinstance(spam_can, Iterable)
会返回 False
,因为 collections.abc.Iterable
只检查 __iter__
方法。
from collections.abc import Iterableclass Spam:def __getitem__(self, i):print('->', i)if i < 2:return ielse:raise IndexErrorspam_can = Spam()
print(iter(spam_can)) # <iterator object at 0x000000000134FF10>
print(isinstance(Spam, Iterable)) # False
2.2 鹅类型(Goose Typing)
与鸭子类型相对,鹅类型是一种更严格的可迭代性定义:
- 只有实现了
__iter__
方法的对象才是可迭代的。
示例:
class GooseSpam:def __iter__(self):return iter([1, 2, 3])from collections import abc
print(issubclass(GooseSpam, abc.Iterable)) # 输出: True
goose_spam_can = GooseSpam()
print(isinstance(goose_spam_can, abc.Iterable)) # 输出: True
在 Python 3.10 中,abc.Iterable
通过实现 __subclasshook__
方法,使得 issubclass
和 isinstance
可以识别实现了 __iter__
方法的类。
这个是13章的知识
3. 何时使用 iter()
进行显式检查
示例:
def process(iterable):if isinstance(iterable, abc.Iterable):for item in iterable:print(item)else:print("对象不可迭代")
在这个例子中,isinstance
检查确保了只有在对象被识别为可迭代时,才会尝试迭代。这在处理来自外部输入或不确定来源的对象时尤其有用,可以提高代码的健壮性。
4. iter()
的第二种用法:使用可调用对象和哨兵值
除了上述的常规用法,iter()
函数还支持另一种更高级的用法,即使用可调用对象(callable)和哨兵值(sentinel)来创建迭代器。
4.1 语法
iter(callable, sentinel)
callable
:一个可调用对象(如函数),它会被反复调用(不接受任何参数)以生成值。sentinel
:一个哨兵值,当可调用对象返回该值时,迭代器会抛出StopIteration
异常,停止迭代。
4.2 示例:掷骰子直到出现 1
from random import randintdef d6():return randint(1, 6)# 使用 iter() 创建迭代器,哨兵值为 1
d6_iter = iter(d6, 1)# 迭代器会持续调用 d6(),直到返回 1
for roll in d6_iter:print(roll)
解释:
d6()
函数模拟掷骰子,每次返回 1 到 6 之间的随机整数。iter(d6, 1)
创建了一个迭代器,该迭代器会反复调用d6()
,并将返回值与哨兵值1
进行比较。- 当
d6()
返回1
时,迭代器会停止迭代,循环结束。
注意:
- 在这个例子中,
1
是哨兵值,因此循环中永远不会打印1
。 - 一旦迭代器被耗尽(即达到哨兵值),它将变得无用。如果需要重新开始迭代,必须重新调用
iter()
创建新的迭代器。
输出示例:
4
3
6
3
(每次运行结果可能不同,因为掷骰子是随机的)
4.3 应用场景:构建块读取器
iter()
的第二种用法在处理文件或数据流时非常有用,例如按固定块大小读取数据,直到文件结束。
示例:
from functools import partialdef process_block(block):print(f"处理块: {block}")with open('mydata.db', 'rb') as f:# 使用 partial 创建一个不接受参数的可调用对象read64 = partial(f.read, 64)# iter() 会反复调用 read64(),直到返回 b''for block in iter(read64, b''):process_block(block)
解释:
partial(f.read, 64)
创建了一个不接受参数的可调用对象,该对象每次调用时会从文件中读取 64 个字节。iter(read64, b'')
创建了一个迭代器,该迭代器会反复调用read64()
,并将返回值与空字节串b''
进行比较。- 当
read64()
返回b''
时,表示文件已读取完毕,迭代器停止迭代。 process_block(block)
函数用于处理每个读取到的数据块。
functools.partial
functools.partial 是 Python 中一个非常有用的函数,它允许你"冻结"函数的一部分参数,从而创建一个新的可调用对象。
基本用法
from functools import partial# 原始函数
def power(base, exponent):return base ** exponent# 创建一个新的平方函数
square = partial(power, exponent=2)
print(square(5)) # 输出: 25 (5的平方)# 创建一个新的立方函数
cube = partial(power, exponent=3)
print(cube(3)) # 输出: 27 (3的立方)
文件读取的例子
回到你提到的文件读取的例子:
from functools import partialwith open('example.txt', 'rb') as f:# 创建一个新的可调用对象,每次调用都会读取64字节read64 = partial(f.read, 64)# 等价于:# def read64():# return f.read(64)# 使用iter来迭代读取,直到返回空字节for block in iter(read64, b''):print(f"读取到 {len(block)} 字节的数据")
优点:
- 简洁性:使用
iter()
可以避免显式编写循环和条件判断,使代码更简洁。 - 可重用性:可以将块读取逻辑封装在可调用对象中,提高代码的可重用性。
4.4. 何时使用 iter()
的第二种用法
- 处理数据流:当需要按块处理数据流(如文件、网络数据等)时,
iter()
提供了一种简洁有效的方法。 - 避免无限循环:使用哨兵值可以防止迭代器进入无限循环,因为当达到特定条件时,迭代会停止。
- 简化代码:相比于手动编写循环和条件判断,使用
iter()
可以使代码更简洁、更具可读性。
5. 总结
iter()
函数是 Python 中实现可迭代性的核心,它通过调用对象的__iter__
或__getitem__
方法来获取迭代器。- 鸭子类型使得 Python 中的可迭代性更加灵活,但
abc.Iterable
提供了更严格的类型检查。 iter()
的第二种用法允许使用可调用对象和哨兵值创建迭代器,适用于处理数据流和避免无限循环等场景。- 显式检查可迭代性在某些情况下是有用的,但通常情况下,直接尝试迭代并处理异常更为高效。
四、Iterables Versus Iterators
一、迭代器与可迭代对象的概念
✅ 口诀记忆:
可迭代对象“能产生”迭代器,
迭代器“能生成”一个一个元素(并记住位置)。
1. 可迭代对象(Iterable)
定义:
任何可以通过内置函数 iter()
获取迭代器的对象都是可迭代对象。具体来说:
- 实现
__iter__
方法: 该方法返回一个迭代器对象。 - 实现
__getitem__
方法: 该方法接受基于 0 的索引。
常见可迭代对象:
- 序列类型: 如字符串 (
str
)、列表 (list
)、元组 (tuple
) 等,因为它们都实现了__iter__
方法。 - 自定义类: 只要实现了
__iter__
或__getitem__
方法,就可以成为可迭代对象。
示例:
# 字符串是可迭代对象
s = 'ABC'
print(iter(s)) # 列表是可迭代对象
lst = [1, 2, 3]
print(iter(lst)) # 自定义可迭代对象
class MyIterable:def __init__(self, data):self.data = datadef __iter__(self):return iter(self.data)my_iterable = MyIterable([4, 5, 6])
print(iter(my_iterable))
<str_iterator object at 0x00000000012FFFD0>
<list_iterator object at 0x00000000012FFFD0>
<list_iterator object at 0x00000000012FFDC0>
2. 迭代器(Iterator)
定义:
迭代器是实现了 __next__
和 __iter__
方法的对象,用于逐一访问可迭代对象的元素。
__next__
方法: 返回序列中的下一个元素,如果没有更多元素,则引发StopIteration
异常。__iter__
方法: 返回迭代器对象本身 (self
),这使得迭代器本身也是可迭代的。
迭代器的工作流程:
- 构建迭代器: 使用
iter()
函数从可迭代对象中获取迭代器。 - 迭代访问: 使用
next()
函数或for
循环逐一获取元素。 - 结束迭代: 当没有更多元素时,
next()
会引发StopIteration
异常,循环结束。
示例:
s = 'ABC'# 使用 for 循环迭代字符串
for char in s:print(char)
# 输出:
# A
# B
# C# 手动模拟 for 循环
it = iter(s)
while True:try:print(next(it))except StopIteration:del itbreak
# 输出:
# A
# B
# C
迭代器与可迭代对象的区别:
- 可迭代对象: 实现了
__iter__
或__getitem__
方法,可以被iter()
函数调用生成迭代器。 - 迭代器: 实现了
__next__
和__iter__
方法,用于遍历可迭代对象。
图示:
图 1:Iterable 和 Iterator 的抽象基类关系
二、迭代器的标准接口
Python 中迭代器的标准接口由 collections.abc.Iterator
抽象基类定义:
__next__
方法: 抽象方法,必须在子类中实现,用于返回下一个元素。__iter__
方法: 返回迭代器对象本身 (self
),允许迭代器在需要可迭代对象的地方使用。
示例:
from collections.abc import Iteratorclass MyIterator(Iterator):def __init__(self, data):self.data = dataself.index = 0def __next__(self):if self.index < len(self.data):result = self.data[self.index]self.index += 1return resultelse:raise StopIterationdef __iter__(self):return self# 使用自定义迭代器
my_iter = MyIterator([7, 8, 9])
for num in my_iter:print(num)
# 输出:
# 7
# 8
# 9
三、迭代器与可迭代对象的关系
- Python 从可迭代对象中获取迭代器。
- 每个迭代器都是可迭代的,因为迭代器实现了
__iter__
方法。 - 但并非所有可迭代对象都是迭代器。 例如,字符串和列表是可迭代对象,但不是迭代器。
示例:
s = 'ABC'# 字符串是可迭代对象,但不是迭代器
print(isinstance(s, Iterator)) # 输出:False 字符串没有实现__next__()方法
print(isinstance(s, Iterable)) # 输出:True# 字符串的迭代器是迭代器,也是可迭代的
it = iter(s)
print(isinstance(it, Iterator)) # 输出:True
print(isinstance(it, Iterable)) # 输出:True
四、迭代器的特点
-
一次性消耗: 迭代器一旦被迭代完,就无法重新迭代,除非重新创建。
s = 'ABC' it = iter(s) print(list(it)) # 输出:['A', 'B', 'C'] print(list(it)) # 输出:[]
-
无法重置: 无法将迭代器重置到初始状态。如果需要重新迭代,必须重新创建迭代器。
s = 'ABC' it = iter(s) next(it) # 输出:'A' it = iter(s) # 重新创建迭代器 next(it) # 输出:'A'
-
节省内存: 迭代器按需生成元素,适用于处理大型数据集或无限序列。
五、工程中常用的迭代器模式
-
惰性求值: 延迟计算,直到需要时才生成元素,节省内存。
class Range:def __init__(self, start, end):self.current = startself.end = enddef __iter__(self):return selfdef __next__(self):if self.current < self.end:result = self.currentself.current += 1return resultelse:raise StopIterationfor num in Range(1, 5):print(num) # 输出: # 1 # 2 # 3 # 4
-
管道处理: 将多个迭代器连接起来,形成数据处理管道。
def filter_even(iterable):for item in iterable:if item % 2 == 0:yield itemdef multiply(iterable, factor):for item in iterable:yield item * factornumbers = [1, 2, 3, 4, 5, 6] result = multiply(filter_even(numbers), 10) print(list(result)) # 输出:[20, 40, 60]
-
无限序列: 创建可以无限迭代的迭代器,例如生成斐波那契数列。
class Fibonacci:def __init__(self):self.a, self.b = 0, 1def __iter__(self):return selfdef __next__(self):result = self.aself.a, self.b = self.b, self.a + self.breturn resultfib = Fibonacci() for _, num in zip(range(10), fib):print(num) # 输出: # 0 # 1 # 1 # 2 # 3 # 5 # 8 # 13 # 21 # 34
六、常见误区与注意事项
-
迭代器与可迭代对象的混淆: 记住,迭代器本身也是可迭代的,但并非所有可迭代对象都是迭代器。
-
迭代器无法重置: 迭代器一旦被迭代完,就无法重新迭代,除非重新创建。
-
避免在循环中修改可迭代对象: 在迭代过程中修改可迭代对象可能导致意想不到的行为。
-
使用
next()
时处理StopIteration
异常: 避免在调用next()
时引发未处理的异常。
五、Sentence Classes with __iter__
1. 可迭代对象与迭代器概述
在 Python 中,可迭代对象(Iterable) 和 迭代器(Iterator) 是实现 迭代器协议(Iterator Protocol) 的两个关键概念:
- 可迭代对象:实现了
__iter__()
方法,该方法返回一个迭代器对象。 - 迭代器:实现了
__next__()
方法,用于逐个返回元素,并在没有更多元素时引发StopIteration
异常。同时,迭代器也实现了__iter__()
方法,通常返回自身。
2. Sentence
类的迭代器模式实现(Sentence Take #2)
2.1 原始代码解析
以下是基于经典迭代器模式的 Sentence
类实现:
import re
import reprlibRE_WORD = re.compile(r'\w+')class Sentence:def __init__(self, text):self.text = textself.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 = wordsself.index = 0def __next__(self):try:word = self.words[self.index]except IndexError:raise StopIteration()self.index += 1return worddef __iter__(self):return self
关键点:
-
__iter__
方法:在Sentence
类中实现,用于返回一个SentenceIterator
实例。这意味着每次调用iter()
都会创建一个新的迭代器。 -
SentenceIterator
类:- 维护一个单词列表
self.words
和一个索引self.index
。 __next__
方法:返回当前索引位置的单词,并递增索引。如果索引超出范围,则引发StopIteration
异常。__iter__
方法:返回自身,使其成为可迭代对象。
- 维护一个单词列表
优点:
- 清晰地分离了可迭代对象和迭代器的职责。
- 符合设计模式中的迭代器模式,易于理解和维护。
缺点:
- 代码较为冗长,需要手动管理迭代器的内部状态。
- 对于简单的迭代需求,显得过于复杂。
2.2 注意事项:不要将可迭代对象自身作为迭代器
错误示例:
class Sentence:def __init__(self, text):self.text = textself.words = text.split()self.index = 0 # 用于记录迭代位置def __iter__(self):return self # 返回自身作为迭代器def __next__(self):if self.index >= len(self.words):raise StopIterationword = self.words[self.index]self.index += 1return word
问题:
- 可迭代对象(如
Sentence
):- 应该是一个稳定的数据容器
- 可以创建多个独立的迭代器
- 生命周期通常较长
- 迭代器:
- 是单次遍历的临时对象
- 需要维护遍历状态(如当前索引)
- 生命周期通常较短
- 符合迭代器模式的设计原则
- 可迭代对象(
Sentence
)负责创建迭代器。 - 迭代器(
SentenceIterator
)负责具体的遍历逻辑。
- 可迭代对象(
具体分析
-
无法支持多次遍历
由于Sentence
类自身就是迭代器,并且__iter__()
返回的是self
,所以一旦迭代过一次,self.index
就会到达末尾。再次尝试迭代时,__next__()
会直接抛出StopIteration
,无法重新开始遍历。s = Sentence("Hello world") list(s) # ['Hello', 'world'] list(s) # [] (无法再次遍历!)
-
状态共享问题
如果多个地方同时迭代同一个Sentence
对象,它们会共享self.index
,导致迭代混乱:s = Sentence("Hello world") it1 = iter(s) it2 = iter(s) # it2 和 it1 共享同一个 index print(next(it1)) # "Hello" print(next(it2)) # "world" (而不是 "Hello")
正确的做法:
- 始终将可迭代对象和迭代器分离,确保每次调用
iter()
都返回一个新的迭代器实例。
3. 使用生成器函数实现 Sentence
类(Sentence Take #3)
3.1 原始代码解析
以下是使用生成器函数的 Sentence
类实现:
import re
import reprlibRE_WORD = re.compile(r'\w+')class Sentence:def __init__(self, text):self.text = textself.words = RE_WORD.findall(text)def __repr__(self):return f'Sentence({reprlib.repr(self.text)})'def __iter__(self):for word in self.words:yield word
关键点:
__iter__
方法:是一个生成器函数,使用yield
关键字逐个生成单词。- 生成器对象:当调用
__iter__()
时,会自动创建一个生成器对象,该对象实现了迭代器协议。
优点:
- 简洁性:无需显式定义迭代器类,代码更加简洁。
- 惰性求值:生成器按需生成值,节省内存,提高效率。
3.2 生成器的工作原理
- 生成器函数:包含
yield
关键字的函数,调用时返回一个生成器对象。
def gen_123():yield 1yield 2yield 3g = gen_123()
print(next(g))
print(next(g))
print(next(g))
print(next(g))# 1
# 2
# 3
# Traceback (most recent call last):
# File "d:\AAAAAstudywork\sec8b\test\testt.py", line 11, in <module>
# print(next(g))
# StopIteration
-
执行流程:
- 调用生成器函数时,函数体不会立即执行,而是返回一个生成器对象。
- 每次调用
next()
时,函数体从上一个yield
位置继续执行,直到下一个yield
。 - 当函数体执行完毕或遇到
return
语句时,生成器对象引发StopIteration
异常。
-
示例:
def gen_AB():print('start')yield 'A'print('continue')yield 'B'print('end.')for c in gen_AB():print('-->', c)
输出:
start
--> A
continue
--> B
end.
解释:
- 第一次调用
next()
时,打印start
,并产生'A'
。 - 第二次调用
next()
时,打印continue
,并产生'B'
。 - 第三次调用
next()
时,打印end.
,函数体执行完毕,引发StopIteration
。
六、Lazy Sentences
一、 概述
本节内容主要围绕 惰性求值(Lazy Evaluation) 在 Python 迭代器中的应用展开,重点介绍了如何利用 re
模块中的惰性函数 re.finditer
以及生成器表达式(Generator Expression)来优化句子解析的实现,使其更加高效、节省内存。
二、 核心概念
1. 惰性求值 vs 急切求值
-
急切求值(Eager Evaluation):
- 立即处理所有数据并生成完整的结果集。
- 优点: 简单直观,易于理解和使用。
- 缺点: 可能会浪费内存和时间,尤其是在处理大型数据集或只需要部分数据时。
- 示例: 使用
re.findall
一次性匹配所有单词并存储在列表中。
-
惰性求值(Lazy Evaluation):
- 仅在需要时处理数据,延迟结果的生成。
- 优点:
- 节省内存:无需一次性存储所有数据。
- 提高效率:避免不必要的计算,尤其适用于处理大型数据集或只需要部分数据的情况。
- 缺点: 实现相对复杂,需要理解迭代器、生成器等概念。
- 示例: 使用
re.finditer
返回一个迭代器,按需生成匹配结果。
2. 迭代器(Iterator)
- 迭代器是实现了
__iter__()
和__next__()
方法的对象,用于逐个访问集合中的元素。 - 迭代器是惰性的,每次调用
next()
方法时才生成下一个元素。 - 优点: 节省内存,适用于处理大型数据集或需要延迟计算的场景。
3. 生成器(Generator)
- 生成器是使用
yield
关键字的函数,每次调用next()
时执行到yield
并暂停执行,返回当前值。 - 生成器是迭代器的一种实现方式,提供了更简洁的语法来创建迭代器。
- 优点:
- 语法简洁,易于编写和理解。
- 内存效率高,适用于处理大型数据集或需要延迟计算的场景。
4. 生成器表达式(Generator Expression)
- 生成器表达式是生成器函数的简洁语法,类似于列表推导式,但使用圆括号
()
而不是方括号[]
。 - 语法:
(表达式 for 变量 in 可迭代对象 if 条件)
- 优点:
- 语法更简洁,易于编写和阅读。
- 内存效率高,与生成器函数一样,是惰性求值的。
三、 详细讲解
1. 原始实现:急切求值
import re
import reprlibRE_WORD = re.compile(r'\w+')class Sentence:def __init__(self, text):self.text = textself.words = RE_WORD.findall(self.text) # 急切地匹配所有单词并存储在列表中def __repr__(self):return f'Sentence({reprlib.repr(self.text)})'def __iter__(self):return iter(self.words) # 返回列表的迭代器
- 问题:
__init__
方法中,RE_WORD.findall(self.text)
会立即匹配所有单词并存储在self.words
列表中。- 对于大型文本,这会占用大量内存,并且如果用户只迭代前几个单词,大部分工作都是不必要的。
2. 惰性实现:使用 re.finditer
class Sentence:def __init__(self, text):self.text = textdef __repr__(self):return f'Sentence({reprlib.repr(self.text)})'def __iter__(self):for match in RE_WORD.finditer(self.text): # 使用 finditer 返回迭代器,按需生成匹配结果yield match.group() # yield 关键字将方法转换为生成器函数
- 改进点:
__iter__
方法使用re.finditer
返回一个迭代器,而不是一次性匹配所有单词。yield
关键字将__iter__
方法转换为生成器函数,使其成为惰性求值。- 优点: 节省内存,避免不必要的计算。
3. 更简洁的惰性实现:使用生成器表达式
class Sentence:def __init__(self, text):self.text = textdef __repr__(self):return f'Sentence({reprlib.repr(self.text)})'def __iter__(self):return (match.group() for match in RE_WORD.finditer(self.text)) # 使用生成器表达式
- 改进点:
__iter__
方法使用生成器表达式(match.group() for match in RE_WORD.finditer(self.text))
来构建生成器对象。- 相比生成器函数,生成器表达式语法更简洁,可读性更高。
- 注意: 生成器表达式总是可以替换为生成器函数,但有时更方便。
4. 对比示例:列表推导式 vs 生成器表达式
def gen_AB():print('start')yield 'A'print('continue')yield 'B'print('end.')# 使用列表推导式
res1 = [x*3 for x in gen_AB()]
print(res1)
# 输出:
# start
# continue
# end.
# ['AAA', 'BBB']
# 列表推导式会立即迭代生成器对象,生成完整的列表for i in res1:print('-->', i)
# 输出:
# --> AAA
# --> BBB# 使用生成器表达式
res2 = (x*3 for x in gen_AB())
print(res2)
# 输出: <generator object <genexpr> at 0x00000000012AA0A0>
# 生成器表达式返回生成器对象,不会立即迭代for i in res2:print('-->', i)
# 输出:
# start
# --> AAA
# continue
# --> BBB
# end.
# 生成器表达式是惰性的,只有在迭代时才执行生成器函数
- 解释:
- 列表推导式会立即迭代生成器对象
gen_AB()
,生成完整的列表res1
,因此会立即输出start
,continue
,end.
。 - 生成器表达式返回生成器对象
res2
,只有在迭代时才执行生成器函数gen_AB()
,因此输出会与for
循环的输出交织在一起。
- 列表推导式会立即迭代生成器对象
四、 实际应用场景
- 处理大型数据集: 例如处理日志文件、数据库查询结果等,使用惰性求值可以避免内存溢出。
- 流式数据处理: 例如处理实时数据流,使用惰性求值可以实现实时处理和响应。
- 需要延迟计算的场景: 例如在用户需要时才进行计算,而不是预先计算所有结果。
五、 注意事项
- 可迭代对象: 惰性求值依赖于可迭代对象,因此需要确保所使用的对象是可迭代的。
- 资源管理: 使用惰性求值时,需要注意资源的释放,例如文件句柄、数据库连接等,避免资源泄漏。
- 性能: 虽然惰性求值可以节省内存,但在某些情况下,频繁的迭代可能会影响性能,需要根据具体情况进行权衡。
七、When to Use Generator Expressions
一、生成器表达式概述
生成器表达式(Generator Expressions)是一种简洁高效的语法,用于创建生成器对象。它类似于列表推导式,但不会立即生成整个列表,而是按需生成元素,节省内存空间。
- 调用包含yield的函数时,不会立即执行函数体,而是返回一个生成器对象。
- 生成器对象实现了迭代器协议(即__next__()方法),可通过next()或循环逐步获取值。
1. 何时使用生成器表达式?
在实现复杂类时,生成器表达式常用于需要迭代或生成数据的场景。例如,在实现 Vector
类时,以下方法都使用了生成器表达式。在第十二章。
对比示例:
# 使用列表推导式
squares = [x * x for x in range(1000000)]# 使用生成器表达式
squares_gen = (x * x for x in range(1000000))# 列表推导式占用大量内存,而生成器表达式几乎不占用内存
2. 语法提示
当生成器表达式作为函数的唯一参数传递时,可以省略函数调用时的括号。例如:
def __mul__(self, scalar):if isinstance(scalar, numbers.Real):return Vector(n * scalar for n in self.components)else:return NotImplemented
但如果有多个参数,则需要使用括号将生成器表达式括起来,以避免语法错误。例如:
def func(a, b, c):pass# 正确用法
func(a, b, (x for x in range(10)))# 错误用法,会导致语法错误
func(a, b, x for x in range(10))
二、生成器函数与生成器表达式的对比
1. 生成器函数
生成器函数是使用 yield
关键字定义的函数,用于创建生成器对象。与生成器表达式相比,生成器函数具有以下优点:
- 灵活性高: 可以包含多条语句和复杂的逻辑。
- 可重用性强: 可以多次调用生成器函数,每次调用都会创建一个新的生成器对象。
- 可作为协程使用: 在 Python 3.5 及以上版本中,生成器函数可以用作协程(后续章节会详细讲解)。
示例:
def countdown(n):while n > 0:yield nn -= 1# 使用生成器函数
for number in countdown(5):print(number)
输出:
5
4
3
2
1
2. 生成器表达式 vs 生成器函数
特性 | 生成器表达式 | 生成器函数 |
---|---|---|
语法简洁度 | 更简洁,适用于简单迭代 | 语法稍复杂,但更灵活 |
逻辑复杂度 | 适用于简单逻辑 | 可处理复杂逻辑 |
可重用性 | 一次性使用 | 可多次调用 |
协程支持 | 不支持 | 支持 |
对比示例:
# 使用生成器表达式
squares_gen = (x * x for x in range(5))for square in squares_gen:print(square)# 使用生成器函数
def squares_func(n):for x in range(n):yield x * xfor square in squares_func(5):print(square)
输出:
0
1
4
9
16
0
1
4
9
16
三、生成器与迭代器的区别
1. 迭代器
迭代器是实现了 __next__
方法的对象,用于在迭代过程中逐步生成数据。迭代器可以由任何对象实现,不一定需要使用生成器。
2. 生成器
生成器是迭代器的一种,由生成器函数或生成器表达式创建。生成器对象实现了 __next__
方法,因此也是迭代器。
关键点:
- 生成器对象: 由生成器函数或生成器表达式返回的对象。
- 生成器函数: 使用
yield
关键字定义的函数,用于创建生成器对象。 - 生成器表达式: 类似于列表推导式,但返回生成器对象。
示例:
# 生成器函数
def my_generator():yield 1yield 2yield 3# 生成器表达式
gen_expr = (x for x in [1, 2, 3])# 两者都是生成器对象
print(type(my_generator())) # <class 'generator'>
print(type(gen_expr)) # <class 'generator'>
3. 术语辨析
- 生成器迭代器: 由生成器函数创建的生成器对象。
- 生成器表达式: 返回一个“迭代器”,但实际上是生成器对象。
四、补充示例
1. 嵌套生成器表达式
# 生成矩阵的转置
matrix = [[1, 2, 3],[4, 5, 6],[7, 8, 9]
]transpose = ((row[i] for row in matrix)for i in range(len(matrix[0]))
)for row in transpose:print(list(row))
输出:
[1, 4, 7]
[2, 5, 8]
[3, 6, 9]
为啥就这个效果了?
这个表达式可以拆解为:
- 外层生成器:
for i in range(len(matrix[0]))
- 内层生成器:
for row in matrix
外层生成器
for i in range(len(matrix[0]))
len(matrix[0])
计算的是矩阵第一行的长度,即列数。在这个例子中,len(matrix[0])
等于3。range(len(matrix[0]))
生成一个范围,从0到2(不包括3),即[0, 1, 2]
。- 因此,外层生成器会依次迭代
i = 0
,i = 1
,i = 2
。
内层生成器
对于每一个 i
,内层生成器执行:
(row[i] for row in matrix)
for row in matrix
会依次迭代矩阵的每一行。- 对于每一行
row
,取row[i]
,即第i
列的元素。
具体执行过程
让我们逐步执行这个生成器表达式,看看它是如何生成转置后的矩阵的。
外层迭代 i = 0
- 内层生成器:
(row[0] for row in matrix)
- 第一行
row = [1, 2, 3]
,取row[0] = 1
- 第二行
row = [4, 5, 6]
,取row[0] = 4
- 第三行
row = [7, 8, 9]
,取row[0] = 7
- 第一行
- 生成的结果:
[1, 4, 7]
外层迭代 i = 1
- 内层生成器:
(row[1] for row in matrix)
- 第一行
row = [1, 2, 3]
,取row[1] = 2
- 第二行
row = [4, 5, 6]
,取row[1] = 5
- 第三行
row = [7, 8, 9]
,取row[1] = 8
- 第一行
- 生成的结果:
[2, 5, 8]
外层迭代 i = 2
- 内层生成器:
(row[2] for row in matrix)
- 第一行
row = [1, 2, 3]
,取row[2] = 3
- 第二行
row = [4, 5, 6]
,取row[2] = 6
- 第三行
row = [7, 8, 9]
,取row[2] = 9
- 第一行
- 生成的结果:
[3, 6, 9]
总结
这个嵌套生成器表达式的关键在于:
- 外层循环控制列索引
i
,确保我们按列的顺序提取元素。 - 内层循环遍历矩阵的每一行,提取第
i
列的元素。 - 通过这种方式,每一列的元素被收集到一个新的子列表中,最终实现了矩阵的转置。
这种使用生成器表达式的嵌套结构不仅简洁,而且高效,因为它避免了创建中间列表,节省了内存空间。
2. 无限生成器
import itertools# 无限生成器
def infinite_gen():while True:yield 1# 使用 itertools.islice 限制生成数量
limited_gen = itertools.islice(infinite_gen(), 5)for num in limited_gen:print(num)
输出:
1
1
1
1
1
八、An Arithmetic Progression Generator
一、 理解算术级数生成器
算术级数是指一组数字,其中每个数字与前一个数字的差值(称为步长)相同。例如:
- 整数级数:0, 1, 2, 3, …
- 浮点数级数:1.0, 1.5, 2.0, 2.5, …
- 分数级数:0, 1/3, 2/3, …
- 十进制级数:0, 0.1, 0.2, …
在编程中,我们常常需要生成这样的序列。Python 内置的 range()
函数可以生成一个有界的整数级数,但如果需要生成其他类型的数字,或者生成无界的序列,range()
就无法满足需求。
二、 使用类实现算术级数生成器
通过定义一个 ArithmeticProgression
类来实现更灵活的算术级数生成器。以下是详细讲解:
1. 构造函数 __init__
class ArithmeticProgression:def __init__(self, begin, step, end=None):self.begin = begin # 序列的起始值self.step = step # 序列的步长self.end = end # 序列的结束值,默认为 None,表示无限序列
-
参数说明:
begin
: 序列的起始值。step
: 序列的步长,可以是任何数值类型(整数、浮点数、分数等)。end
: 序列的结束值,默认为None
,表示生成无限序列。
-
设计思路:
- 与
range()
不同,ArithmeticProgression
将step
设为必填参数,而end
为可选参数,更符合算术级数的定义。 - 参数名称从
start/stop
改为begin/end
,以突出与range()
的不同。
- 与
2. 迭代器 __iter__
def __iter__(self):result_type = type(self.begin + self.step) # 确定序列中数字的类型result = result_type(self.begin) # 初始化第一个值,类型与后续数字一致forever = self.end is None # 判断是否为无限序列index = 0while forever or result < self.end:yield resultindex += 1result = self.begin + self.step * index
- 关键点:
- 类型转换:
- 为了确保序列中所有数字类型一致,使用
result_type = type(self.begin + self.step)
来确定类型。 - 例如,如果
begin
是int
,step
是float
,则result_type
为float
,所有后续数字都将转换为float
类型。 - 这种方式避免了使用已废弃的
coerce()
函数,而是利用了 Python 的数值强制转换规则。
- 为了确保序列中所有数字类型一致,使用
- 避免浮点数累积误差:
- 每次迭代时,通过
self.begin + self.step * index
计算当前值,而不是在之前的结果上累加step
。
class ArithmeticProgressionAccumulate:def __init__(self, begin, step, end=None):self.begin = beginself.step = stepself.end = enddef __iter__(self):result = self.beginindex = 0while self.end is None or result < self.end:yield resultindex += 1result += self.step # 累加步长# 使用示例 ap_accumulate = ArithmeticProgressionAccumulate(0, 0.1, 1) print(list(ap_accumulate))# [0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6, 0.7, 0.7999999999999999, 0.8999999999999999, 0.9999999999999999]class ArithmeticProgression:def __init__(self, begin, step, end=None):self.begin = begin # 序列的起始值self.step = step # 序列的步长self.end = end # 序列的结束值,默认为 None,表示无限序列def __iter__(self):result_type = type(self.begin + self.step) # 确定序列中数字的类型result = result_type(self.begin) # 初始化第一个值,类型与后续数字一致forever = self.end is None # 判断是否为无限序列index = 0while forever or result < self.end:yield resultindex += 1result = self.begin + self.step * index# 使用示例 ap_accumulate = ArithmeticProgression(0, 0.1, 1) print(list(ap_accumulate)) # [0.0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8, 0.9]
- 每次迭代时,通过
- 无限序列处理:
- 当
end
为None
时,forever
标志为True
,循环将一直执行,直到手动停止迭代。
- 当
- 类型转换:
3. 使用示例
>>> 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')]
- 说明:
- 上述示例展示了
ArithmeticProgression
可以生成不同类型的数字序列,包括整数、浮点数、分数和十进制数。 - 序列的类型由
begin + step
的结果决定,符合 Python 的数值强制转换规则。
- 上述示例展示了
三、 使用生成器函数简化实现
虽然 ArithmeticProgression
类实现了所需的功能,但原文指出,如果类的唯一目的是构建生成器,那么可以用生成器函数来代替。
1. aritprog_gen
生成器函数
def aritprog_gen(begin, step, end=None):result = type(begin + step)(begin)forever = end is Noneindex = 0while forever or result < end:yield resultindex += 1result = begin + step * index
-
与类的对比:
- 逻辑与
ArithmeticProgression
类中的__iter__
方法几乎相同。 - 优点在于代码更简洁,不需要定义一个完整的类。
- 逻辑与
-
使用示例:
>>> list(aritprog_gen(0, 1, 3))
[0, 1, 2]>>> list(aritprog_gen(1, .5, 3))
[1.0, 1.5, 2.0, 2.5]
四、 利用 itertools
模块实现算术级数
Python 的 itertools
模块提供了许多强大的生成器函数,可以用来简化生成器的实现。
1. itertools.count
- 功能:返回一个无限生成器,从指定的起始值开始,以指定的步长递增。
- 示例:
>>> import itertools
>>> gen = itertools.count(1, .5)
>>> next(gen)
1
>>> next(gen)
1.5
>>> next(gen)
2.0
>>> next(gen)
2.5
- 注意事项:
itertools.count
生成的序列是无限的,调用list(count())
会导致程序崩溃,因为它会尝试创建一个包含所有可能数字的列表。
2. itertools.takewhile
- 功能:返回一个生成器,它会消耗另一个生成器,并在给定的谓词(predicate)返回
False
时停止。 - 示例:
>>> gen = itertools.takewhile(lambda n: n < 3, itertools.count(1, .5))
>>> list(gen)
[1, 1.5, 2.0, 2.5]
3. 结合 itertools
实现算术级数
import itertoolsdef aritprog_gen(begin, step, end=None):first = type(begin + step)(begin)ap_gen = itertools.count(first, step)if end is None:return ap_genreturn itertools.takewhile(lambda n: n < end, ap_gen)
-
说明:
itertools.count
用于生成无限序列。itertools.takewhile
用于限制序列的结束值。- 这种方式避免了手动管理索引和循环,代码更加简洁。
-
与
aritprog_gen
生成器函数的对比:- 两者功能相同,但使用
itertools
的版本更简洁。 - 需要注意的是,
itertools.count
同样存在浮点数累积误差的问题。
- 两者功能相同,但使用
五、 总结
-
选择合适的工具:
- 对于简单的算术级数生成,使用生成器函数或
itertools
模块会更加简洁高效。 - 对于需要更复杂逻辑或自定义行为的场景,使用类来实现生成器可能更合适。
- 对于简单的算术级数生成,使用生成器函数或
-
注意数值类型:
- 始终注意序列中数字的类型,避免因类型不匹配导致的错误。
- 可以利用 Python 的数值强制转换规则来确保类型的一致性。
-
避免浮点数误差:
- 尽量避免在循环中重复累加步长,使用基于索引的计算方式可以减少误差。
-
利用标准库:
- 熟悉
itertools
模块中可用的生成器函数,可以帮助我们更高效地实现各种生成器。
- 熟悉
练习题
- 编写一个生成器函数,生成一个斐波那契数列。
- 使用
ArithmeticProgression
类或aritprog_gen
函数生成一个从 10 开始,以 0.1 为步长,直到 20 的序列。 - 修改
ArithmeticProgression
类,使其能够处理负数步长。 - 编写一个生成器函数,生成一个等比数列(Geometric Progression)。
- 使用
itertools
模块生成一个从 100 开始,以 2 为步长,直到 200 的序列。
九、Generator Functions in the Standard Library
1. 过滤生成器函数(Filtering Generator Functions)
过滤生成器函数 的主要功能是从输入的可迭代对象中筛选出符合条件的元素,不改变元素本身。这些函数通常接受一个谓词(predicate),即一个接受单个参数并返回布尔值的函数,用于判断元素是否应包含在输出中。
1.1 主要函数
模块 | 函数 | 描述 |
---|---|---|
itertools | compress(it, selector_it) | 并行消耗两个可迭代对象;当 selector_it 中对应元素为真时,yield it 中的元素 |
itertools | dropwhile(predicate, it) | 消耗 it ,跳过谓词为真的元素,然后 yield 剩余的所有元素(不再进行谓词检查) |
内置函数 | filter(predicate, it) | 对 it 中的每个元素应用谓词,yield 谓词为真的元素;如果 predicate 为 None ,则仅 yield 真值元素 |
itertools | filterfalse(predicate, it) | 与 filter 相反,当谓词为假时 yield 元素 |
itertools | islice(it, stop) 或 islice(it, start, stop, step=1) | 从 it 的切片中 yield 元素,类似于 s[:stop] 或 s[start:stop:step] ,但适用于任何可迭代对象,且操作是惰性的 |
itertools | takewhile(predicate, it) | 当谓词为真时 yield 元素,然后停止,不再进行进一步检查 |
2.2 示例
def vowel(c):return c.lower() in 'aeiou'# 使用 filter 过滤元音字母
print(list(filter(vowel, 'Aardvark'))) # 输出: ['A', 'a', 'a']# 使用 itertools.filterfalse 过滤非元音字母
import itertools
print(list(itertools.filterfalse(vowel, 'Aardvark'))) # 输出: ['r', 'd', 'v', 'r', 'k']# 使用 itertools.dropwhile 跳过开头的元音字母
print(list(itertools.dropwhile(vowel, 'Aardvark'))) # 输出: ['r', 'd', 'v', 'a', 'r', 'k']# 使用 itertools.takewhile 获取开头的元音字母
print(list(itertools.takewhile(vowel, 'Aardvark'))) # 输出: ['A', 'a']# 使用 itertools.compress 根据选择器筛选元素
print(list(itertools.compress('Aardvark', (1, 0, 1, 1, 0, 1)))) # 输出: ['A', 'r', 'd', 'a']# 使用 itertools.islice 获取前 4 个元素
print(list(itertools.islice('Aardvark', 4))) # 输出: ['A', 'a', 'r', 'd']# 使用 itertools.islice 获取索引 4 到 6 的元素
print(list(itertools.islice('Aardvark', 4, 7))) # 输出: ['v', 'a', 'r']# 使用 itertools.islice 获取索引 1 到 6,步长为 2 的元素
print(list(itertools.islice('Aardvark', 1, 7, 2))) # 输出: ['a', 'd', 'a']
3. 映射生成器函数(Mapping Generator Functions)
映射生成器函数 对输入可迭代对象中的每个元素(或多个可迭代对象中的对应元素)进行计算,并 yield 计算后的结果。
3.1 主要函数
模块 | 函数 | 描述 |
---|---|---|
itertools | accumulate(it, [func]) | 累积求和;如果提供 func ,则对每对相邻元素应用 func ,并 yield 结果 |
内置函数 | enumerate(iterable, start=0) | yield 包含索引和值的元组,索引从 start 开始 |
内置函数 | map(func, it1, [it2, …, itN]) | 对 it1 中的每个元素应用 func ,并 yield 结果;如果提供多个可迭代对象,func 必须接受相应数量的参数,且可迭代对象会并行消耗 |
itertools | starmap(func, it) | 对 it 中的每个元素应用 func ,并 yield 结果;输入可迭代对象应 yield 可迭代的元素,func 将以解包的方式应用 |
3.2 示例
# 使用 itertools.accumulate 计算累积和
sample = [5, 4, 2, 8, 7, 6, 3, 0, 9, 1]
import itertools
print(list(itertools.accumulate(sample))) # 输出: [5, 9, 11, 19, 26, 32, 35, 35, 44, 45]# 使用 itertools.accumulate 计算累积最小值
print(list(itertools.accumulate(sample, min))) # 输出: [5, 4, 2, 2, 2, 2, 2, 0, 0, 0]# 使用 itertools.accumulate 计算累积最大值
print(list(itertools.accumulate(sample, max))) # 输出: [5, 5, 5, 8, 8, 8, 8, 8, 9, 9]# 使用 itertools.accumulate 计算累积乘积
import operator
print(list(itertools.accumulate(sample, operator.mul))) # 输出: [5, 20, 40, 320, 2240, 13440, 40320, 0, 0, 0]# 使用 itertools.accumulate 计算阶乘
print(list(itertools.accumulate(range(1, 11), operator.mul))) # 输出: [1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800]# 使用 enumerate 对字母进行编号,从 1 开始
print(list(enumerate('albatroz', 1))) # 输出: [(1, 'a'), (2, 'l'), (3, 'b'), (4, 'a'), (5, 't'), (6, 'r'), (7, 'o'), (8, 'z')]# 使用 map 计算平方
print(list(map(operator.mul, range(11), range(11)))) # 输出: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100]# 使用 map 并行处理多个可迭代对象
print(list(map(operator.mul, range(11), [2, 4, 8]))) # 输出: [0, 4, 16]# 使用 map 返回元组
print(list(map(lambda a, b: (a, b), range(11), [2, 4, 8]))) # 输出: [(0, 2), (1, 4), (2, 8)]# 使用 itertools.starmap 对 enumerate 的结果进行计算
print(list(itertools.starmap(operator.mul, enumerate('albatroz', 1)))) # 输出: ['a', 'll', 'bbb', 'aaaa', 'ttttt', 'rrrrrr', 'ooooooo', 'zzzzzzzz']# 使用 itertools.starmap 计算运行平均值
sample = [5, 4, 2, 8, 7, 6, 3, 0, 9, 1]
print(list(itertools.starmap(lambda a, b: b / a, enumerate(itertools.accumulate(sample), 1)))) # 输出: [5.0, 4.5, 3.6666666666666665, 4.75, 5.2, 5.333333333333333, 5.0, 4.375, 4.888888888888889, 4.5]
4. 合并生成器函数(Merging Generator Functions)
合并生成器函数 从多个输入可迭代对象中并行或顺序地 yield 元素。
4.1 主要函数
模块 | 函数 | 描述 |
---|---|---|
itertools | chain(it1, …, itN) | 顺序地 yield it1 , it2 , …, itN 中的所有元素 |
itertools | chain.from_iterable(it) | 顺序地 yield it 中每个可迭代对象的所有元素 |
itertools | product(it1, …, itN, repeat=1) | 计算笛卡尔积,yield 由输入可迭代对象中元素组合而成的 N 元组;repeat 参数允许重复使用输入可迭代对象 |
内置函数 | zip(it1, …, itN, strict=False) | 并行地 yield 由输入可迭代对象中元素组成的 N 元组;当任意一个可迭代对象耗尽时停止,除非 strict=True ,则当长度不一致时引发 ValueError |
itertools | zip_longest(it1, …, itN, fillvalue=None) | 并行地 yield 由输入可迭代对象中元素组成的 N 元组,直到所有可迭代对象都耗尽;用 fillvalue 填充缺失的值 |
4.2 示例
# 使用 itertools.chain 连接多个可迭代对象
print(list(itertools.chain('ABC', range(2)))) # 输出: ['A', 'B', 'C', 0, 1]# 使用 itertools.chain 连接 enumerate 的结果
print(list(itertools.chain(enumerate('ABC')))) # 输出: [(0, 'A'), (1, 'B'), (2, 'C')]# 使用 itertools.chain.from_iterable 连接 enumerate 的结果
print(list(itertools.chain.from_iterable(enumerate('ABC')))) # 输出: [0, 'A', 1, 'B', 2, 'C']# 使用 zip 并行处理多个可迭代对象
print(list(zip('ABC', range(5), [10, 20, 30, 40]))) # 输出: [('A', 0, 10), ('B', 1, 20), ('C', 2, 30)]# 使用 itertools.zip_longest 并行处理多个可迭代对象,指定填充值
print(list(itertools.zip_longest('ABC', range(5), fillvalue='?')) # 输出: [('A', 0), ('B', 1), ('C', 2), ('?', 3), ('?', 4)]# 使用 itertools.product 计算笛卡尔积
print(list(itertools.product('ABC', range(2)))) # 输出: [('A', 0), ('A', 1), ('B', 0), ('B', 1), ('C', 0), ('C', 1)]# 使用 itertools.product 计算笛卡尔积,指定重复次数
print(list(itertools.product('ABC', repeat=2))) # 输出: [('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'B'), ('B', 'C'), ('C', 'A'), ('C', 'B'), ('C', 'C)]# 使用 itertools.product 计算笛卡尔积,重复使用输入可迭代对象
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)
5. 扩展生成器函数(Expanding Generator Functions)
扩展生成器函数 将每个输入元素扩展为多个输出元素。
5.1 主要函数
模块 | 函数 | 描述 |
---|---|---|
itertools | combinations(it, out_len) | 从 it 中 yield 由 out_len 个元素组成的组合 |
itertools | combinations_with_replacement(it, out_len) | 从 it 中 yield 由 out_len 个元素组成的组合,包括重复元素的组合 |
itertools | count(start=0, step=1) | 从 start 开始,以 step 为步长,无限地 yield 数字 |
itertools | cycle(it) | 无限地重复 yield it 中的元素,同时存储每个元素的副本 |
itertools | pairwise(it) | 从输入可迭代对象中 yield 连续的重叠对(Python 3.10 新增) |
itertools | permutations(it, out_len=None) | 从 it 中 yield 由 out_len 个元素组成的排列;默认情况下,out_len 为 len(list(it)) |
itertools | repeat(item, [times]) | 无限地重复 yield 给定的元素,除非指定了重复次数 |
5.2 示例
# 使用 itertools.combinations 生成组合
print(list(itertools.combinations('ABC', 2))) # 输出: [('A', 'B'), ('A', 'C'), ('B', 'C')]# 使用 itertools.combinations_with_replacement 生成包含重复元素的组合
print(list(itertools.combinations_with_replacement('ABC', 2))) # 输出: [('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'B'), ('B', 'C'), ('C', 'C')]# 使用 itertools.permutations 生成排列
print(list(itertools.permutations('ABC', 2))) # 输出: [('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]# 使用 itertools.product 生成笛卡尔积,模拟 permutations
print(list(itertools.product('ABC', repeat=2))) # 输出: [('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'B'), ('B', 'C'), ('C', 'A'), ('C', 'B'), ('C', 'C')]# 使用 itertools.count 构建一个倒计时生成器
ct = itertools.count(10, -1)
print(next(ct)) # 输出: 10
print(next(ct)) # 输出: 9
print(next(ct)) # 输出: 8# 使用 itertools.cycle 构建一个循环生成器
cy = itertools.cycle('ABC')
print(next(cy)) # 输出: 'A'
print(next(cy)) # 输出: 'B'
print(next(cy)) # 输出: 'C'
print(next(cy)) # 输出: 'A'# 使用 itertools.pairwise 获取连续的重叠对
print(list(itertools.pairwise(range(7))) # 输出: [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)]# 使用 itertools.repeat 构建一个重复生成器
rp = itertools.repeat(7)
print(next(rp)) # 输出: 7
print(next(rp)) # 输出: 7# 使用 itertools.repeat 与 map 结合使用,提供固定参数
print(list(map(operator.mul, range(11), itertools.repeat(5))) # 输出: [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50]
6. 重排生成器函数(Rearranging Generator Functions)
重排生成器函数 对输入可迭代对象的元素进行重新排列。
6.1 主要函数
模块 | 函数 | 描述 |
---|---|---|
itertools | groupby(it, key=None) | yield 形如 (key, group) 的元组,其中 key 是分组依据,group 是一个生成器,yield 分组中的元素 |
内置函数 | reversed(seq) | 倒序 yield seq 中的元素;seq 必须是序列或实现 __reversed__ 特殊方法 |
itertools | tee(it, n=2) | yield 一个包含 n 个生成器的元组,每个生成器独立地 yield 输入可迭代对象的元素 |
6.2 示例
# 使用 itertools.groupby 进行分组
print(list(itertools.groupby('LLLLAAGGG'))) # 输出: [('L', <itertools._grouper object at 0x102227cc0>), ('A', <itertools._grouper object at 0x102227b38>), ('G', <itertools._grouper object at 0x102227b70>)]# 使用 itertools.groupby 展开分组结果
for char, group in itertools.groupby('LLLLAAAGG'):print(char, '->', list(group))
# 输出:
# L -> ['L', 'L', 'L', 'L']
# A -> ['A', 'A',]
# G -> ['G', 'G', 'G']# 使用 itertools.groupby 对动物列表进行分组
animals = ['duck', 'eagle', 'rat', 'giraffe', 'bear', 'bat', 'dolphin', 'shark', 'lion']
animals.sort(key=len)
print(animals) # 输出: ['rat', 'bat', 'duck', 'bear', 'lion', 'eagle', 'shark', 'giraffe', 'dolphin']for length, group in itertools.groupby(animals, len):print(length, '->', list(group))
# 输出:
# 3 -> ['rat', 'bat']
# 4 -> ['duck', 'bear', 'lion']
# 5 -> ['eagle', 'shark']
# 7 -> ['giraffe', 'dolphin']# 使用 itertools.groupby 对倒序后的动物列表进行分组
for length, group in itertools.groupby(reversed(animals), len):print(length, '->', list(group))
# 输出:
# 7 -> ['dolphin', 'giraffe']
# 5 -> ['shark', 'eagle']
# 4 -> ['lion', 'bear', 'duck']
# 3 -> ['bat', 'rat']# 使用 itertools.tee 生成多个独立的生成器
print(list(itertools.tee('ABC'))) # 输出: [<itertools._tee object at 0x10222abc8>, <itertools._tee object at 0x10222ac08>]g1, g2 = itertools.tee('ABC')
print(next(g1)) # 输出: 'A'
print(next(g2)) # 输出: 'A'
print(next(g2)) # 输出: 'B'
print(list(g1)) # 输出: ['B', 'C']
print(list(g2)) # 输出: ['C']print(list(zip(*itertools.tee('ABC'))) # 输出: [('A', 'A'), ('B', 'B'), ('C', 'C')]
7. 总结
标准库中的生成器函数为处理各种数据提供了强大的工具:
- 过滤函数 允许我们筛选出符合特定条件的元素。
- 映射函数 让我们能够对元素进行各种计算和转换。
- 合并函数 提供了将来自多个可迭代对象的元素进行组合和配对的能力。
- 扩展函数 让我们能够将每个元素扩展为在输出中产生多个值。
- 重排函数 提供了对元素进行重新排列和分组的手段。
这些函数可以组合使用,以实现复杂的数据处理逻辑。例如,groupby
可以与 filter
结合使用,以过滤掉某些组,或者与 map
结合使用,以对每个组应用某种转换。
十、Iterable Reducing Functions
一、概述
本章将深入探讨 Python 中的一类特殊函数:迭代器归约函数(Iterable Reducing Functions)。这些函数具有以下特点:
- 输入:一个可迭代对象(iterable),如列表、元组、生成器等。
- 输出:单个结果。
- 作用:通过对可迭代对象中的元素进行累积计算,最终得到一个综合性的结果。
正因为这些特点,它们也被称为“归约(reducing)”、“折叠(folding)”或“累积(accumulating)”函数。
二、常见的内置归约函数
Python 提供了多个内置的归约函数,它们在日常编程中非常常用。以下是这些函数的详细讲解:
1. all(it)
-
功能:判断可迭代对象
it
中的所有元素是否都为真值(truthy)。- 真值定义:在布尔上下文中为
True
的值,例如非零数字、非空容器等。
- 真值定义:在布尔上下文中为
-
返回值:
- 如果所有元素都为真值,返回
True
。 - 如果可迭代对象为空,返回
True
(因为没有元素不满足条件)。 - 否则,返回
False
。
- 如果所有元素都为真值,返回
-
示例:
>>> all([1, 2, 3]) True >>> all([1, 0, 3]) False >>> all([]) True
-
应用场景:
- 检查列表中所有元素是否满足某个条件,例如检查所有考试成绩是否及格。
scores = [88, 92, 79, 85] if all(score >= 60 for score in scores):print("所有学生都及格了!") else:print("有学生不及格。")
2. any(it)
-
功能:判断可迭代对象
it
中是否存在至少一个真值元素。 -
返回值:
- 如果存在至少一个真值元素,返回
True
。 - 如果可迭代对象为空,返回
False
。 - 否则,返回
False
。
- 如果存在至少一个真值元素,返回
-
示例:
>>> any([1, 2, 3]) True >>> any([1, 0, 3]) True >>> any([0, 0.0]) False >>> any([]) False
-
应用场景:
- 检查列表中是否存在满足某个条件的元素,例如检查是否有学生成绩超过 90 分。
scores = [88, 92, 79, 85] if any(score > 90 for score in scores):print("有学生成绩超过 90 分!") else:print("所有学生成绩都低于或等于 90 分。")
-
优化机制:
any
和all
都支持短路计算(short-circuiting):- 对于
any
,一旦遇到第一个真值元素,就会立即返回True
,无需遍历剩余元素。 - 对于
all
,一旦遇到第一个假值元素,就会立即返回False
,无需遍历剩余元素。
- 对于
- 这种机制提高了效率,尤其是在处理大型数据集时。
>>> g = (n for n in [0, 0.0, 7, 8]) >>> any(g) True >>> next(g) 8
- 在上述例子中,
any
在遇到7
时就返回True
,因此8
仍然保留在生成器g
中。
3. max(it, key=None, default=None)
-
功能:返回可迭代对象
it
中的最大值。- 参数:
key
:一个函数,用于指定排序的键。例如,key=len
表示按字符串长度排序。default
:如果可迭代对象为空,则返回该值。
- 参数:
-
返回值:
- 返回最大的元素。
- 如果可迭代对象为空且指定了
default
,则返回default
。 - 否则,引发
ValueError
。
-
示例:
>>> max([1, 3, 2]) 3 >>> max([], default=0) 0 >>> max(["apple", "banana", "cherry"], key=len) 'banana'
-
应用场景:
- 查找列表中的最高分。
- 查找字典中值最大的键值对。
scores = {"Alice": 88, "Bob": 92, "Charlie": 79} top_student = max(scores, key=scores.get) print(f"最高分的学生是 {top_student},分数是 {scores[top_student]}.")
4. min(it, key=None, default=None)
-
功能:返回可迭代对象
it
中的最小值。- 参数:
- 与
max
相同。
- 与
- 参数:
-
返回值:
- 与
max
相同。
- 与
-
示例:
>>> min([1, 3, 2]) 1 >>> min([], default=0) 0 >>> min(["apple", "banana", "cherry"], key=len) 'apple'
5. functools.reduce(func, it, initial=None)
-
功能:将可迭代对象
it
中的元素按照二元函数func
进行累积计算。- 参数:
func
:一个接受两个参数的函数,例如lambda x, y: x + y
。it
:可迭代对象。initial
:可选的初始值,用于在归约开始前与第一个元素进行计算。
- 参数:
-
返回值:
- 最终的累积结果。
-
工作原理:
- 依次将前两个元素传递给
func
,将结果与下一个元素继续传递给func
,直到所有元素都被处理。 - 如果提供了
initial
,则首先将initial
与第一个元素传递给func
,然后继续处理后续元素。
- 依次将前两个元素传递给
-
示例:
>>> from functools import reduce >>> reduce(lambda x, y: x + y, [1, 2, 3, 4]) 10 >>> reduce(lambda x, y: x * y, [1, 2, 3, 4], 10) 240
-
应用场景:
- 计算列表中所有元素的乘积。
- 计算阶乘。
from functools import reduce def factorial(n):return reduce(lambda x, y: x * y, range(1, n+1)) print(factorial(5)) # 输出 120
-
注意事项:
functools.reduce
相比其他归约函数更通用,但代码可读性较低。- 对于简单的归约操作,建议使用
sum
,all
,any
等更直观的函数。
6. sum(it, start=0)
-
功能:计算可迭代对象
it
中所有元素的总和。- 参数:
start
:可选的起始值,默认为0
。
- 参数:
-
返回值:
- 所有元素与
start
的总和。
- 所有元素与
-
示例:
>>> sum([1, 2, 3, 4]) 10 >>> sum([1, 2, 3, 4], 10) 20
-
应用场景:
- 计算列表中所有数字的总和。
- 计算列表中所有字符串的长度总和。
lengths = [len(s) for s in ["apple", "banana", "cherry"]] total_length = sum(lengths) print(f"总长度为 {total_length}.")
-
注意事项:
- 对于浮点数计算,建议使用
math.fsum
以获得更高的精度。
import math numbers = [0.1, 0.2, 0.3] print(sum(numbers)) # 输出 0.6000000000000001 print(math.fsum(numbers)) # 输出 0.6
- 对于浮点数计算,建议使用
三、sorted
函数与归约函数
sorted
函数虽然不是归约函数,但它与归约函数一样,都需要遍历整个可迭代对象。sorted
会将所有元素读取到内存中并排序,然后返回一个新的列表。- 与之相比,
reversed
是一个生成器函数,不会将所有元素加载到内存中,而是返回一个反向的迭代器。
十一、Subgenerators with yield from
1. 引言
在 Python 3.3 中,引入了 yield from
语法,用于简化生成器(generator)之间的协作,使得一个生成器可以将工作委托给子生成器(subgenerator)。本笔记将深入浅出地讲解 yield from
的概念、应用场景,并通过多个实际例子帮助理解。
2. yield from
的基本概念
2.1 背景:使用 for
循环委托生成器
在 yield from
出现之前,如果一个生成器需要委托另一个生成器来产生值,通常会使用 for
循环。例如:
def sub_gen():yield 1.1yield 1.2def gen():yield 1for i in sub_gen():yield iyield 2for x in gen():print(x)
输出:
1
1.1
1.2
2
解释:
gen
生成器首先产生1
,然后通过for
循环遍历sub_gen()
产生的值1.1
和1.2
,最后产生2
。
2.2 使用 yield from
简化委托
使用 yield from
可以更简洁地实现上述功能:
def sub_gen():yield 1.1yield 1.2def gen():yield 1yield from sub_gen()yield 2for x in gen():print(x)
输出:
1
1.1
1.2
2
解释:
yield from sub_gen()
相当于将sub_gen
生成器的所有产出值直接传递给gen
的调用者。gen
在执行yield from
时会暂停,直到sub_gen
耗尽。
优点:
- 代码更简洁易读。
- 避免了显式的
for
循环,使生成器之间的委托关系更加清晰。
3. 深入理解 yield from
3.1 yield from
的工作原理
- 暂停委托生成器: 当
gen
执行yield from sub_gen()
时,gen
会暂停执行,控制权转移到sub_gen
。 - 子生成器接管:
sub_gen
开始执行,产生的值直接传递给gen
的调用者。 - 委托生成器恢复: 当
sub_gen
耗尽后,gen
恢复执行,继续执行yield from
之后的代码。
注意:
- 在
yield from
执行期间,委托生成器无法访问通过yield from
传递的值。 - 只有当子生成器完成执行后,委托生成器才会继续执行。
3.2 捕获子生成器的返回值
如果子生成器包含 return
语句,yield from
可以捕获其返回值。例如:
def sub_gen():yield 1.1yield 1.2return 'Done!'def gen():yield 1result = yield from sub_gen()print('<--', result)yield 2for x in gen():print(x)
输出:
1
1.1
1.2
<-- Done!
2
解释:
sub_gen
返回字符串'Done!'
,该值被yield from
捕获并赋值给变量result
。gen
打印出<-- Done!
后继续执行,产生2
。
4. 实际应用案例
4.1 重构 itertools.chain
itertools.chain
可以将多个可迭代对象串联起来。yield from
可以简化其实现。
原始实现:
def chain(*iterables):for it in iterables:for i in it:yield is = 'ABC'
r = range(3)
print(list(chain(s, r)))
输出:
['A', 'B', 'C', 0, 1, 2]
使用 yield from
重构:
def chain(*iterables):for i in iterables:yield from is = 'ABC'
r = range(3)
print(list(chain(s, r)))
输出:
['A', 'B', 'C', 0, 1, 2]
解释:
yield from i
替代了内部的for
循环,使代码更简洁。- 但在这个例子中,
yield from
的优势并不明显,主要是为了展示其用法。
4.2 遍历树结构
yield from
在处理递归数据结构(如树)时非常有用。以下以 Python 的异常层次结构为例,展示如何使用 yield from
进行深度优先遍历。
步骤 1:仅显示根类
def tree(cls):yield cls.__name__def display(cls):for cls_name in tree(cls):print(cls_name)if __name__ == '__main__':display(BaseException)
输出:
BaseException
步骤 2:显示根类和直接子类
走到for cls_name, level in tree(cls):
后,走yield cls.__name__, 0
,print,然后走到for sub_cls in cls.__subclasses__():
,走yield sub_cls.__name__, 1
,print。依次类推。
def tree(cls):yield cls.__name__, 0for sub_cls in cls.__subclasses__():yield sub_cls.__name__, 1def display(cls):for cls_name, level in tree(cls):indent = ' ' * 4 * levelprint(f'{indent}{cls_name}')if __name__ == '__main__':display(BaseException)
输出:
BaseExceptionExceptionGeneratorExitSystemExitKeyboardInterrupt
步骤 3:递归显示子类
def tree(cls):yield cls.__name__, 0yield from sub_tree(cls, 1)def sub_tree(cls, level):for sub_cls in cls.__subclasses__():yield sub_cls.__name__, levelyield from sub_tree(sub_cls, level + 1)def display(cls):for cls_name, level in tree(cls):indent = ' ' * 4 * levelprint(f'{indent}{cls_name}')if __name__ == '__main__':display(BaseException)
输出:
BaseExceptionExceptionTypeErrorStopAsyncIterationStopIteration...GeneratorExitSystemExitKeyboardInterrupt
解释:
sub_tree
函数递归地遍历子类。yield from sub_tree(sub_cls, level + 1)
实现了递归调用,将当前子类作为新的根类,并增加层级。- 通过
yield from
,代码避免了显式的递归调用,使逻辑更加清晰。
步骤 4:合并 tree
和 sub_tree
def tree(cls, level=0):yield cls.__name__, levelfor sub_cls in cls.__subclasses__():yield from tree(sub_cls, level + 1)def display(cls):for cls_name, level in tree(cls):indent = ' ' * 4 * levelprint(f'{indent}{cls_name}')if __name__ == '__main__':display(BaseException)
解释:
- 将
tree
和sub_tree
合并为一个函数tree
,参数level
用于跟踪当前层级。 yield from tree(sub_cls, level + 1)
实现了递归调用。- 这种方式更加简洁,逻辑也更清晰。
5. 注意事项
- 递归深度限制: 使用
yield from
进行递归遍历时,需要注意 Python 的递归深度限制(默认 1000)。对于非常深的树结构,可能会导致RecursionError
。 - 避免无限递归: 确保有适当的终止条件,避免无限递归。例如,在遍历树时,当没有更多子类时,递归调用不会发生。
6. 总结
yield from
是一种强大的工具,可以简化生成器之间的协作,使代码更加简洁和易于维护。通过理解其工作原理和应用场景,可以更好地利用 yield from
来处理复杂的生成器逻辑,例如递归遍历树结构。
十二、Generic Iterable Types
一、概述
在 Python 中,**可迭代对象(Iterable)和迭代器(Iterator)**是处理数据集合的重要概念。Python 的标准库提供了许多接受可迭代对象作为参数的函数。为了提高代码的可读性和可维护性,我们可以使用类型注解来明确这些函数的参数和返回值的类型。
二、可迭代对象(Iterable)
1. 概念
- 可迭代对象是能够返回迭代器的对象。
- 常见的可迭代对象包括列表、元组、字典、集合、字符串等。
2. 类型注解
- 使用
collections.abc.Iterable
或typing.Iterable
进行类型注解。typing.Iterable
在 Python 3.9 及更早版本中使用。- 从 Python 3.10 开始,
collections.abc.Iterable
是更推荐的方式。
3. 示例解析
原始示例:zip_replace 函数
from collections.abc import Iterable
FromTo = tuple[str, str]def zip_replace(text: str, changes: Iterable[FromTo]) -> str:for from_, to in changes:text = text.replace(from_, to)return text
解析:
-
类型别名
FromTo
:- 使用
tuple[str, str]
定义了一个包含两个字符串的元组类型。 - 从 Python 3.10 开始,建议使用
typing.TypeAlias
来明确这是一个类型别名:FromTo: TypeAlias = tuple[str, str]
- 作用:提高类型注解的可读性。
- 使用
-
参数
changes
:- 被注解为
Iterable[FromTo]
,表示它是一个可迭代对象,其元素是FromTo
类型的元组。 - 例如,
changes
可以是[(“a”, “b”), (“c”, “d”)]
或任何其他可迭代的FromTo
元组。
- 被注解为
-
函数功能:
- 遍历
changes
中的每个(from_, to)
元组,使用str.replace()
方法将text
中的from_
字符串替换为to
字符串。 - 返回修改后的字符串。
- 遍历
补充示例:使用列表作为 changes
参数
changes = [("hello", "hi"), ("world", "earth")]
result = zip_replace("hello world", changes)
print(result) # 输出: "hi earth"
解释:
changes
是一个列表,符合Iterable[FromTo]
的要求。- 函数将
"hello"
替换为"hi"
,将"world"
替换为"earth"
,最终返回"hi earth"
。
三、迭代器(Iterator)
1. 概念
- 迭代器是实现了
__iter__()
和__next__()
方法的对象。 - 迭代器用于遍历可迭代对象,每次调用
__next__()
方法返回下一个值,直到没有元素可返回时抛出StopIteration
异常。
2. 类型注解
- 使用
collections.abc.Iterator
或typing.Iterator
进行类型注解。- 与
Iterable
类似,typing.Iterator
在 Python 3.9 及更早版本中使用。 - 从 Python 3.10 开始,推荐使用
collections.abc.Iterator
。
- 与
3. 生成器与迭代器
- 生成器是使用
yield
关键字的函数或生成器表达式。 - 生成器实现了迭代器协议,因此可以将其视为一种特殊的迭代器。
- 在类型注解中:
- 使用
Iterator[T]
注解生成器函数或生成器表达式。 Iterator[T]
是Generator[T, None, None]
的简写形式,表示生成器只产生类型为T
的值,不消费或返回其他值。
- 使用
4. 示例解析
原始示例:斐波那契数列生成器
def fibonacci() -> Iterator[int]:a, b = 0, 1while True:yield a # 生成当前的斐波那契数,并暂停,等待下一次调用a, b = b, a + b # 更新斐波那契数的下一个值# 方法一:使用 for 循环和 enumerate
for i, n in enumerate(fibonacci()):# 1. `enumerate(fibonacci())` 的执行:# - 调用 `fibonacci()`,创建一个生成器对象,用于生成斐波那契数。# - `enumerate` 将生成器对象转换为一个新的生成器,该生成器生成 (索引, 值) 的元组。# - 例如,第一次调用时返回 (0, 0),第二次调用时返回 (1, 1),依此类推。# 2. `for` 循环的第一次迭代:# - `enumerate` 调用 `fibonacci()` 生成器的 `__next__()` 方法,获取第一个值 `0`。# - `enumerate` 返回 `(0, 0)`。# - `i` 被赋值为 `0`,`n` 被赋值为 `0`。# - 检查 `i >= 20`,因为 `0 >= 20` 为 `False`,所以执行 `print(n)`,输出 `0`。if i >= 20:break # 当索引达到 20 时,退出循环print(n) # 输出当前的斐波那契数# 3. `for` 循环的后续迭代:# - 每次迭代,`enumerate` 会调用 `fibonacci()` 生成器的 `__next__()` 方法,获取下一个值。# - 例如,第二次迭代时,`fibonacci()` 生成 `1`,`enumerate` 返回 `(1, 1)`。# - `i` 被赋值为 `1`,`n` 被赋值为 `1`,输出 `1`。# - 依此类推,直到 `i` 达到 `20`。# 4. 循环终止:# - 当 `i` 达到 `20` 时,`if i >= 20` 条件为 `True`,执行 `break`,退出循环。# - 这意味着 `fibonacci()` 生成器在生成第 21 个值之前被终止。# 5. 生成器的惰性求值:# - `fibonacci()` 是一个生成器函数,采用惰性求值。# - `enumerate` 每次迭代时只从生成器中获取下一个值,而不是一次性获取所有值。# - 例如:# - 第一次迭代时,`fibonacci()` 生成 `0`。# - 第二次迭代时,`fibonacci()` 生成 `1`。# - 第三次迭代时,`fibonacci()` 生成 `1`。# - 依此类推。# - 这种方式节省内存并提高效率,特别适用于处理大量数据或无限序列。# 方法二:使用 next() 和一个生成器对象
fib_gen = fibonacci() # 1. 调用 fibonacci(),创建一个生成器对象 fib_gen# - 此时,生成器函数内的代码尚未执行,等待第一次调用 next()# 包含 yield 的函数:# - 如果函数体内有 yield 语句,Python 会将该函数标记为一个生成器函数(generator function)。生成器函数在调用时不会立即执行,而是返回一个生成器对象(generator object)。# 不包含 yield 的函数:# 如果函数体内没有 yield 语句,Python 会将其视为普通的函数(function)。普通函数在调用时会立即执行,并返回其结果。for i in range(20): # 2. 开始一个循环,循环次数为 20 次print(next(fib_gen)) # 3. 在每次循环中,执行以下步骤:# a. 调用 next(fib_gen):# - 这会触发生成器 fib_gen 执行,直到遇到下一个 yield 语句。# - 第一次调用时,生成器执行到第一个 yield,生成 `0` 并暂停。# - 第二次调用时,生成器从上次暂停的地方继续执行,生成 `1` 并再次暂停。# - 依此类推,每次调用 next() 都会生成下一个斐波那契数。# b. print() 函数输出生成的斐波那契数。# 例如:# - 第一次循环输出 `0`# - 第二次循环输出 `1`# - 第三次循环输出 `1`# - 依此类推,直到第 20 次循环# 4. 生成器的状态管理:# - 生成器 fib_gen 会记住上一次执行的位置。# - 每次调用 next(fib_gen) 时,生成器从上一次暂停的地方继续执行。# - 例如:# - 第一次调用 next(fib_gen) 后,生成器状态为 a=1, b=1。# - 第二次调用 next(fib_gen) 后,生成器状态为 a=1, b=2。# - 依此类推。
解析:
-
返回类型注解:
Iterator[int]
表示生成器产生的值是整数。- 等价于
Generator[int, None, None]
。
-
函数功能:
- 无限生成斐波那契数列。
- 每次调用
next()
返回下一个斐波那契数。
示例:使用生成器表达式
from collections.abc import Iterator # 导入 Iterator 类,用于类型注解
from keyword import kwlist # 导入 kwlist,常量列表,包含所有 Python 关键字
from typing import TYPE_CHECKING # 导入 TYPE_CHECKING,用于类型检查
from typing import reveal_type # 导入 reveal_type,用于在类型检查时显示类型信息# 生成器表达式,用于筛选长度小于 5 的 Python 关键字
short_kw = (k for k in kwlist if len(k) < 5)# 当进行类型检查时(通常由静态类型检查工具如 Mypy 触发)
if TYPE_CHECKING:# 使用 reveal_type() 来显示 short_kw 的推断类型# 输出: typing.Generator[builtins.str*, None, None]# 解释:# - typing.Generator 表示这是一个生成器类型# - [builtins.str*] 表示生成器产生的每个元素都是 str 类型# - 后面的两个 None 分别表示 send() 方法的参数类型和返回类型,这里不使用,所以为 Nonereveal_type(short_kw) # 揭示 short_kw 的类型# 使用显式的类型注解,将生成器对象注解为 Iterator[str]
long_kw: Iterator[str] = (k for k in kwlist if len(k) >= 4)# 当进行类型检查时
if TYPE_CHECKING:# 使用 reveal_type() 来显示 long_kw 的推断类型# 输出: typing.Iterator[builtins.str]# 解释:# - typing.Iterator 表示这是一个迭代器类型# - [builtins.str] 表示迭代器产生的每个元素都是 str 类型reveal_type(long_kw) # 揭示 long_kw 的类型
解析:
-
生成器表达式
short_kw
:- 产生长度小于 5 的 Python 关键字。
- Mypy 推断其类型为
typing.Generator[builtins.str*, None, None]
,其中*
表示不消费或返回其他值。
-
显式类型注解
long_kw
:- 使用
Iterator[str]
进行注解。 - Mypy 确认
Iterator[str]
与Generator[str, None, None]
一致,因此不会报错。
- 使用
对比示例:使用 Iterator
和 Generator
进行注解
from collections.abc import Iterator, Generatordef generator_func() -> Generator[int, None, None]:yield 1def iterator_func() -> Iterator[int]:yield 1
解释:
generator_func
使用Generator[int, None, None]
进行注解,明确表示生成器不消费或返回其他值。iterator_func
使用Iterator[int]
进行注解,简洁地表示生成器只产生整数。
注意:
- 虽然两者在功能上等价,但
Iterator[T]
更加简洁,推荐在不需要明确生成器其他特性的情况下使用。
四、工程中常用的点
-
选择合适的可迭代类型:
- 根据具体需求选择
Iterable
,Iterator
,Generator
等类型进行注解。 - 例如,如果函数内部需要多次遍历输入数据,使用
Iterable
更为合适;如果只需要单次遍历,使用Iterator
更为高效。
- 根据具体需求选择
-
使用类型别名提高可读性:
- 对于复杂的类型,可以使用类型别名来简化类型注解,提高代码可读性。
-
理解
Iterator
与Generator
的关系:Iterator[T]
是Generator[T, None, None]
的简写形式。- 了解两者的区别和联系,有助于更准确地使用类型注解。
-
利用类型检查工具:
- 使用 Mypy 等类型检查工具,可以帮助捕捉类型错误,提高代码质量。
十三、Classic Coroutines
暂时不写(更关注当前版本的原生协程)