当前位置: 首页 > news >正文

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']
    
    1. 匹配过程
    • “Hello”:匹配 \w+,因为 “Hello” 是连续的字母。
    • “,”:不匹配 \w+,因为逗号不是字母、数字或下划线。
    • " world":匹配 \w+,因为 “world” 是连续的字母。
    • “!”:不匹配 \w+,因为感叹号不是字母、数字或下划线。
    1. 结果
    • 因此,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__ 方法。以下是迭代过程的详细步骤:

  1. 调用 iter(s):

    • Python 会调用 s.__iter__() 方法(如果存在)。
    • 如果 __iter__ 方法不存在,Python 会退而求其次,尝试调用 s.__getitem__ 方法,从索引 0 开始依次访问元素,直到引发 StopIteration 异常。
  2. 迭代过程:

    • 第一次调用 s.__getitem__(0),返回第一个单词。
    • 第二次调用 s.__getitem__(1),返回第二个单词。
    • 以此类推,直到所有单词都被访问完毕。
    • 当索引超出范围时,Python 会引发 StopIteration 异常,结束迭代。
    >>> s = Sentence("Hello, world!")
    >>> for word in s:
    ...     print(word)
    Hello
    world
    
  3. 执行顺序

  • 只要存在__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() 函数的工作机制如下:

  1. 检查 __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
  1. 回退到 __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
  1. 无法迭代时抛出 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 序列(如 listtuplestr 等)都是可迭代的,因为它们都实现了 __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️⃣ 结合代码执行流程

步骤操作输出/效果临时列表状态
1spam_can[0]打印 -> 0,返回 0[0]
2spam_can[1]打印 -> 1,返回 1[0, 1]
3spam_can[2]打印 -> 2,抛出 IndexError循环终止

4️⃣ 最终结果

  • 临时列表 _temp_list 最终为 [0, 1]
  • 这个值被赋给 result 变量,即 result = [0, 1]

💡 关键结论

  1. 临时列表的来源

    • list() 函数内部隐式创建的,用于暂存迭代过程中获取的元素。
    • 它本质是一个动态增长的Python列表,通过 append() 逐步填充。
  2. 你的 __getitem__ 如何驱动这个过程

    • Python 自动从 i=0 开始调用,直到遇到 IndexError
    • 每次返回值都会被追加到临时列表
  3. 为什么是 [0, 1]

    • 因为 i=0i=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__ 方法,使得 issubclassisinstance 可以识别实现了 __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),这使得迭代器本身也是可迭代的。

迭代器的工作流程:

  1. 构建迭代器: 使用 iter() 函数从可迭代对象中获取迭代器。
  2. 迭代访问: 使用 next() 函数或 for 循环逐一获取元素。
  3. 结束迭代: 当没有更多元素时,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

四、迭代器的特点

  1. 一次性消耗: 迭代器一旦被迭代完,就无法重新迭代,除非重新创建。

    s = 'ABC'
    it = iter(s)
    print(list(it))  # 输出:['A', 'B', 'C']
    print(list(it))  # 输出:[]
    
  2. 无法重置: 无法将迭代器重置到初始状态。如果需要重新迭代,必须重新创建迭代器。

    s = 'ABC'
    it = iter(s)
    next(it)  # 输出:'A'
    it = iter(s)  # 重新创建迭代器
    next(it)  # 输出:'A'
    
  3. 节省内存: 迭代器按需生成元素,适用于处理大型数据集或无限序列。

五、工程中常用的迭代器模式

  1. 惰性求值: 延迟计算,直到需要时才生成元素,节省内存。

    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
    
  2. 管道处理: 将多个迭代器连接起来,形成数据处理管道。

    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]
    
  3. 无限序列: 创建可以无限迭代的迭代器,例如生成斐波那契数列。

    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
    

六、常见误区与注意事项

  1. 迭代器与可迭代对象的混淆: 记住,迭代器本身也是可迭代的,但并非所有可迭代对象都是迭代器。

  2. 迭代器无法重置: 迭代器一旦被迭代完,就无法重新迭代,除非重新创建。

  3. 避免在循环中修改可迭代对象: 在迭代过程中修改可迭代对象可能导致意想不到的行为。

  4. 使用 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

问题:

  1. 可迭代对象(如Sentence):
    • 应该是一个稳定的数据容器
    • 可以创建多个独立的迭代器
    • 生命周期通常较长
  2. 迭代器
    • 是单次遍历的临时对象
    • 需要维护遍历状态(如当前索引)
    • 生命周期通常较短
  3. 符合迭代器模式的设计原则
    • 可迭代对象(Sentence)负责创建迭代器。
    • 迭代器(SentenceIterator)负责具体的遍历逻辑。

具体分析

  1. 无法支持多次遍历
    由于 Sentence 类自身就是迭代器,并且 __iter__() 返回的是 self,所以一旦迭代过一次,self.index 就会到达末尾。再次尝试迭代时,__next__() 会直接抛出 StopIteration,无法重新开始遍历。

    s = Sentence("Hello world")
    list(s)  # ['Hello', 'world']
    list(s)  # []  (无法再次遍历!)
    
  2. 状态共享问题
    如果多个地方同时迭代同一个 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
  • 执行流程

    1. 调用生成器函数时,函数体不会立即执行,而是返回一个生成器对象。
    2. 每次调用 next() 时,函数体从上一个 yield 位置继续执行,直到下一个 yield
    3. 当函数体执行完毕或遇到 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]

为啥就这个效果了?

这个表达式可以拆解为:

  1. 外层生成器for i in range(len(matrix[0]))
  2. 内层生成器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() 不同,ArithmeticProgressionstep 设为必填参数,而 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) 来确定类型。
      • 例如,如果 beginintstepfloat,则 result_typefloat,所有后续数字都将转换为 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]
      
    • 无限序列处理
      • endNone 时,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 模块中可用的生成器函数,可以帮助我们更高效地实现各种生成器。

练习题

  1. 编写一个生成器函数,生成一个斐波那契数列。
  2. 使用 ArithmeticProgression 类或 aritprog_gen 函数生成一个从 10 开始,以 0.1 为步长,直到 20 的序列。
  3. 修改 ArithmeticProgression 类,使其能够处理负数步长。
  4. 编写一个生成器函数,生成一个等比数列(Geometric Progression)。
  5. 使用 itertools 模块生成一个从 100 开始,以 2 为步长,直到 200 的序列。

九、Generator Functions in the Standard Library

1. 过滤生成器函数(Filtering Generator Functions)

过滤生成器函数 的主要功能是从输入的可迭代对象中筛选出符合条件的元素,不改变元素本身。这些函数通常接受一个谓词(predicate),即一个接受单个参数并返回布尔值的函数,用于判断元素是否应包含在输出中。

1.1 主要函数

模块函数描述
itertoolscompress(it, selector_it)并行消耗两个可迭代对象;当 selector_it 中对应元素为真时,yield it 中的元素
itertoolsdropwhile(predicate, it)消耗 it,跳过谓词为真的元素,然后 yield 剩余的所有元素(不再进行谓词检查)
内置函数filter(predicate, it)it 中的每个元素应用谓词,yield 谓词为真的元素;如果 predicateNone,则仅 yield 真值元素
itertoolsfilterfalse(predicate, it)filter 相反,当谓词为假时 yield 元素
itertoolsislice(it, stop)islice(it, start, stop, step=1)it 的切片中 yield 元素,类似于 s[:stop]s[start:stop:step],但适用于任何可迭代对象,且操作是惰性的
itertoolstakewhile(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 主要函数
模块函数描述
itertoolsaccumulate(it, [func])累积求和;如果提供 func,则对每对相邻元素应用 func,并 yield 结果
内置函数enumerate(iterable, start=0)yield 包含索引和值的元组,索引从 start 开始
内置函数map(func, it1, [it2, …, itN])it1 中的每个元素应用 func,并 yield 结果;如果提供多个可迭代对象,func 必须接受相应数量的参数,且可迭代对象会并行消耗
itertoolsstarmap(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 主要函数
模块函数描述
itertoolschain(it1, …, itN)顺序地 yield it1, it2, …, itN 中的所有元素
itertoolschain.from_iterable(it)顺序地 yield it 中每个可迭代对象的所有元素
itertoolsproduct(it1, …, itN, repeat=1)计算笛卡尔积,yield 由输入可迭代对象中元素组合而成的 N 元组;repeat 参数允许重复使用输入可迭代对象
内置函数zip(it1, …, itN, strict=False)并行地 yield 由输入可迭代对象中元素组成的 N 元组;当任意一个可迭代对象耗尽时停止,除非 strict=True,则当长度不一致时引发 ValueError
itertoolszip_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 主要函数
模块函数描述
itertoolscombinations(it, out_len)it 中 yield 由 out_len 个元素组成的组合
itertoolscombinations_with_replacement(it, out_len)it 中 yield 由 out_len 个元素组成的组合,包括重复元素的组合
itertoolscount(start=0, step=1)start 开始,以 step 为步长,无限地 yield 数字
itertoolscycle(it)无限地重复 yield it 中的元素,同时存储每个元素的副本
itertoolspairwise(it)从输入可迭代对象中 yield 连续的重叠对(Python 3.10 新增)
itertoolspermutations(it, out_len=None)it 中 yield 由 out_len 个元素组成的排列;默认情况下,out_lenlen(list(it))
itertoolsrepeat(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 主要函数
模块函数描述
itertoolsgroupby(it, key=None)yield 形如 (key, group) 的元组,其中 key 是分组依据,group 是一个生成器,yield 分组中的元素
内置函数reversed(seq)倒序 yield seq 中的元素;seq 必须是序列或实现 __reversed__ 特殊方法
itertoolstee(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 分。")
    
  • 优化机制

    • anyall 都支持短路计算(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.11.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:合并 treesub_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)

解释:

  • treesub_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.Iterabletyping.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.Iteratortyping.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] 一致,因此不会报错。

对比示例:使用 IteratorGenerator 进行注解

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] 更加简洁,推荐在不需要明确生成器其他特性的情况下使用。

四、工程中常用的点

  1. 选择合适的可迭代类型:

    • 根据具体需求选择 Iterable, Iterator, Generator 等类型进行注解。
    • 例如,如果函数内部需要多次遍历输入数据,使用 Iterable 更为合适;如果只需要单次遍历,使用 Iterator 更为高效。
  2. 使用类型别名提高可读性:

    • 对于复杂的类型,可以使用类型别名来简化类型注解,提高代码可读性。
  3. 理解 IteratorGenerator 的关系:

    • Iterator[T]Generator[T, None, None] 的简写形式。
    • 了解两者的区别和联系,有助于更准确地使用类型注解。
  4. 利用类型检查工具:

    • 使用 Mypy 等类型检查工具,可以帮助捕捉类型错误,提高代码质量。

十三、Classic Coroutines

暂时不写(更关注当前版本的原生协程)

相关文章:

  • 《饶议科学》阅读笔记
  • Qt开发经验 --- 避坑指南(5)
  • OpenCV-Python (官方)中文教程(部分一)_Day21
  • IT行业词汇科普手册
  • 对京东开展外卖业务的一些思考
  • DeepSeek全域智能革命:从量子纠缠到星际文明的认知跃迁引言:认知边界的坍缩与重构
  • 发那科机器人3(机器人编程基础)
  • Linux/AndroidOS中进程间的通信线程间的同步 - 共享内存
  • Kafka的核心组件有哪些?简要说明其作用。 (Producer、Consumer、Broker、Topic、Partition、ZooKeeper)
  • STM32开发printf函数支持
  • LabVIEW 与 NI 硬件(PXI, CompactRIO, DAQ, RF, Vision)的深度研究与未来发展趋势-分析报告
  • 【AI】模型与权重的基本概念
  • LeetCode热题100--73.矩阵置零--中等
  • JC/T 2187-2013 铝波纹芯复合铝板检测
  • 如何保证Kafka生产者的消息顺序性? (单分区内有序,需确保同一Key的消息发送到同一分区)
  • IBM BAW(原BPM升级版)使用教程Toolkit介绍
  • C语言--字符函数
  • 前端面试每日三题 - Day 27
  • 【“星睿O6”评测】Armv9.2a、KLEIDIAI及vulkan加速llamacpp部署本地AI
  • 数据清洗-电商双11美妆数据分析(二)
  • 阶跃星辰CEO姜大昕:追求智能上限仍是最重要的事,多模态的“GPT-4时刻”尚未到来
  • 母亲节书单|关于生育自由的未来
  • 综艺还有怎样的新可能?挖掘小众文化领域
  • 总导演揭秘十五运会闭幕式:赴一场星辰大海之约
  • “苏河超级管”调研:桥下公园“留白”很好,指引差点
  • 东方红资管官宣:41岁原国信资管董事长成飞出任新总经理