首页 › 论坛 › 置顶 › Python的秘密生活:迭代器协议 – 为什么for循环如此神奇
-
作者帖子
-
2025-11-11 11:37 #27334Q QPY课程团队管理员
蒂莫西正在向一位来自C++的同事解释Python时遇到了困惑。“在Python中,你可以遍历列表、字典、文件、字符串、范围、集合……那
for是如何知道如何处理这些不同类型的呢?”玛格丽特听到后微笑着说:“这就是迭代器协议——Python最优雅的设计之一。每种与
for一起工作的类型都使用相同的语言,你也可以教你的对象这门语言。让我来给你展示一下这个魔法。”谜题:for循环适用于所有类型
蒂莫西向玛格丽特展示了让他困惑的内容:
def demonstrate_for_loop_versatility(): """for循环适用于如此多不同的类型!""" # 遍历一个列表 for item in [1, 2, 3]:print(item, end=' ') print("← 列表") # 遍历字符串 for char in "hello": print(char, end=' ') print("← 字符串") # 遍历字典for key in {'a': 1, 'b': 2}: print(key, end=' ') print("← dict keys") # 遍历文件 with open('example.txt', 'w') as f:f.write('line1nline2nline3') with open('example.txt') as f: for line in f: print(line.strip(), end=' ') print("← 文件行") # 遍历一个范围 for num in range(3):print(num, end=' ') print("← range") # 遍历一个集合 for item in {10, 20, 30}: print(item, end=' ') print("← set") demonstrate_for_loop_versatility()输出:1 2 3 ← 列表 h e l l o ← 字符串 a b ← 字典键 line1 line2 line3 ← 文件行 0 1 2 ← 范围 10 20 30 ← 集合“看到了吗?”蒂莫西指着说。“这些不同类型的
for循环语法是完全相同的。Python是如何知道如何遍历每一个的?”迭代器协议:Python的迭代契约
玛格丽特勾勒出了这个概念:
""" 迭代器协议:两个简单的方法使迭代工作 可迭代对象:任何具有 __iter__() 方法的对象 - 返回一个迭代器 迭代器:任何具有 __next__() 方法的对象 - 返回下一个项目 - 完成时引发 StopIteration - 还应该具有 __iter__() 方法,返回自身 FOR 循环契约: 当 Python 看到: for item in obj: 它实际上做的是: iterator = iter(obj) # 调用 obj.__iter__() while True: try: item = next(iterator) # 调用 iterator.__next__() # 循环体 except StopIteration: break 每个与 'for' 一起工作的对象都实现了这个协议! """ def demonstrate_for_loop_expansion():"""展示 for 循环的真实作用""" items = [1, 2, 3] print("使用 for 循环:") for item in items: print(f" {item}") print("nPython 实际上做了什么:") iterator = iter(items) # 获取迭代器 while True: try:item = next(iterator) # 获取下一个项目 print(f" {item}") except StopIteration: # 没有更多项目 break print("n✓ 结果相同!") demonstrate_for_loop_expansion()输出:
使用 for 循环: 1 2 3 Python 实际执行的: 1 2 3 ✓ 相同的结果!从零开始构建一个简单的迭代器
“让我来告诉你如何创建你自己的迭代器,”玛格丽特说。
class CountDown: """简单的迭代器,从 n 倒计时到 1""" def __init__(self, start): self.current = start def __iter__(self): """返回迭代器对象(self)""" return self def __next__(self): """返回下一个值或引发 StopIteration""" if self.current <= 0: raise StopIterationvalue = self.current self.current -= 1 return value def demonstrate_custom_iterator(): """使用我们的自定义迭代器""" print("倒计时:") for num in CountDown(5): print(num, end=' ') print("n") # 也展示了手动迭代的有效性print("手动迭代:") countdown = CountDown(3) print(f" next(): {next(countdown)}") print(f" next(): {next(countdown)}") print(f" next(): {next(countdown)}") try:next(countdown) # 应该引发 StopIteration except StopIteration: print(" StopIteration 被引发 - 完成!") demonstrate_custom_iterator()输出:
倒计时: 5 4 3 2 1 手动迭代: next(): 3 next(): 2 next(): 1 StopIteration 被引发 - 完成!蒂莫西研究了代码。“所以迭代器记住它的位置——
current值——每次我调用next()时,它都会前进并返回下一个值。”“没错,”玛格丽特确认道。“但是这个设计有一个限制。看看如果你尝试迭代两次会发生什么。”
她快速输入:
countdown = CountDown(3) print("第一次循环:")for num in countdown: print(num, end=' ') print("nn第二个循环:") for num in countdown: print(num, end=' ') # 这会有效吗?输出:
第一次循环: 3 2 1 第二次循环:“第二次循环什么都没有!”蒂莫西惊呼道。“为什么?”
“因为迭代器在第一次循环中耗尽了。
current的值现在是 0,并且保持为 0。如果你想再次迭代,你需要一个新的迭代器。这就是我们需要区分 可迭代对象 和 迭代器 的地方。”区分可迭代对象和迭代器
玛格丽特解释了一个重要的区别:
""" 最佳实践:区分可迭代对象和迭代器 可迭代对象:可以被迭代的容器 迭代器:实际进行迭代的对象 这允许: - 多个同时迭代 - 重用可迭代对象 - 更清晰的设计 """ class CountDown: """创建倒计时迭代器的可迭代对象""" def __init__(self, start): self.start = startdef __iter__(self): """每次返回一个新的迭代器""" return CountDownIterator(self.start) class CountDownIterator: """实际的迭代器""" def __init__(self, start): self.current = start def __iter__(self): """迭代器应该返回自身""" return self def __next__(self):"""返回下一个值""" if self.current <= 0: raise StopIteration value = self.current self.current -= 1 return value def demonstrate_multiple_iterations(): """展示为什么分离可迭代对象/迭代器很重要""" countdown = CountDown(3) print("第一次迭代:") for num in countdown:print(num, end=' ') print() print("n第二次迭代(因为我们得到了一个新的迭代器!):") for num in countdown: print(num, end=' ') print() print("n多个同时迭代:") iter1 = iter(countdown)iter2 = iter(countdown) print(f" iter1: {next(iter1)}, {next(iter1)}") print(f" iter2: {next(iter2)}, {next(iter2)}") print(" ✓ 独立的迭代器!") demonstrate_multiple_iterations()输出:
第一次迭代: 3 2 1 第二次迭代(有效,因为我们得到了一个新的迭代器!): 3 2 1 多个同时迭代: iter1: 3, 2 iter2: 3, 2 ✓ 独立的迭代器!内置可迭代对象的解析
蒂莫西想了解内置类型是如何工作的。“那么这些类型——列表、字符串、字典——它们都实现了迭代器协议吗?”
“每一个都实现了,”玛格丽特确认道。“让我给你展示一下你每天使用的这些类型的内部工作原理。”
def explore_builtin_iterables(): """理解内置类型如何实现迭代""" # 列表 my_list = [1, 2, 3] print("列表:")print(f" Has __iter__: {hasattr(my_list, '__iter__')}") print(f" iter() returns: {type(iter(my_list))}") # 字符串 my_string = "hello" print("n字符串:")print(f" Has __iter__: {hasattr(my_string, '__iter__')}") print(f" iter() returns: {type(iter(my_string))}") # 字典 my_dict = {'a': 1, 'b': 2} print("n字典:")print(f" 是否具有 __iter__: {hasattr(my_dict, '__iter__')}") print(f" iter() 返回: {type(iter(my_dict))}") print(f" 默认迭代: {list(my_dict)}") # 键print(f" 值: {list(my_dict.values())}") print(f" 项目: {list(my_dict.items())}") # 文件 with open('temp.txt', 'w') as f: f.write('line1nline2')with open('temp.txt') as f: print("n文件:") print(f" 是否具有 __iter__: {hasattr(f, '__iter__')}") print(f" 迭代内容: lines") explore_builtin_iterables()蒂莫西研究了输出结果。“所以它们都有
__iter__,但每个返回不同类型的迭代器 –list_iterator、str_iterator、dict_keyiterator。Python 为每种内置类型提供了专门的迭代器。”“没错,”玛格丽特说。“注意字典的一个有趣之处 – 默认情况下,它们是遍历键的,但你也可以获取值或项的迭代器。相同的协议,不同的迭代器。”
“这很优雅,”蒂莫西观察到。“一个协议,但每种类型以适合该类型的方式实现它。”
“现在让我给你展示如何构建你自己的可迭代集合,”玛格丽特说,同时打开一个新文件。
真实世界用例 1:自定义集合
“好的,”提摩太说,“我理解这个协议。但我在实际代码中如何使用它呢?”
玛格丽特微笑着说:“这是个很好的问题。让我们构建一个实用的东西——一个可以迭代的音乐播放列表。”
class Playlist: """可以迭代的音乐播放列表""" def __init__(self, name): self.name = name self.songs = [] def add_song(self, song): self.songs.append(song)def __iter__(self): """返回歌曲的迭代器""" return iter(self.songs) # 委托给列表的迭代器 def __len__(self): return len(self.songs) def demonstrate_custom_collection(): """展示自定义可迭代集合""" playlist = Playlist("最爱") playlist.add_song("歌曲 A")playlist.add_song("歌曲 B") playlist.add_song("歌曲 C") print(f"播放列表: {playlist.name}") print(f" 歌曲数量 ({len(playlist)}):") for song in playlist:print(f" - {song}") # 适用于所有迭代工具 print(f"n 第一首歌: {next(iter(playlist))}") print(f" 所有歌曲: {list(playlist)}") demonstrate_custom_collection()输出:
播放列表:最爱 歌曲 (3): - 歌曲 A - 歌曲 B - 歌曲 C 第一首歌:歌曲 A 所有歌曲:['歌曲 A', '歌曲 B', '歌曲 C']蒂莫西运行了代码,满意地点了点头。“这很简洁。Playlist 类是可迭代的,所以我可以在
for循环、list()、next()中使用它——任何期望可迭代对象的地方。我只是委托给了列表的迭代器,而不是自己编写一个。”“没错,”玛格丽特确认道。“你并不总是需要编写自定义迭代器类。如果你有一个内部集合,只需委托给它的迭代器。但有时你需要更多的控制……”
现实世界用例 2:分页
“这是一个更复杂的模式,”玛格丽特说,调出一个新示例。“想象一下,你正在从一个返回分页结果的 API 获取数据。你不想一次性将所有内容加载到内存中——你希望根据需要获取每一页。”
蒂莫西向前倾了倾。“像懒加载吗?”
“正是如此。看看这个:”
class PaginatedAPI: """用于分页 API 结果的迭代器"""def __init__(self, page_size=10, total_items=100): self.page_size = page_size self.total_items = total_items self.current_item = 0 def __iter__(self): return self def __next__(self): if self.current_item >= self.total_items: raise StopIteration这段代码定义了一个类的初始化方法、迭代器方法和下一个元素的方法。初始化方法接受两个参数:`page_size`(每页大小,默认为10)和`total_items`(总项数,默认为100)。在迭代器方法中,返回自身以支持迭代,而在`__next__`方法中,当当前项数大于或等于总项数时,抛出`StopIteration`异常以结束迭代。
# 模拟获取一页结果
page_start = self.current_item
page_end = min(self.current_item + self.page_size, self.total_items)items = list(range(page_start, page_end))
self.current_item = page_endreturn items
def demonstrate_pagination():
“””展示分页迭代器“””print("正在分页获取结果:") api = PaginatedAPI(page_size=25, total_items=87) for page_num, page in enumerate(api, 1):print(f" 页码 {page_num}: {len(page)} 项目(首个: {page[0]}, 最后: {page[-1]})") print(f"n✓ 已成功获取所有项目,而无需将所有内容加载到内存中!") demonstrate_pagination()输出:
分批获取结果: 第1页:25项(首项:0,末项:24) 第2页:25项(首项:25,末项:49) 第3页:25项(首项:50,末项:74) 第4页:12项(首项:75,末项:86) ✓ 成功获取所有项而无需将所有内容加载到内存中!“太棒了!”蒂莫西惊呼道。“每次调用
next()都会获取下一页。我并不是一次性加载所有87个项目,而是每次加载25个。这在真实的API中也能工作。”“没错。这就是数据库游标的工作原理,API客户端如何处理分页,以及你如何处理大型数据集。迭代器协议使得懒加载变得自然而然。”
“迭代器可以是无限的吗?”蒂莫西突然问道。
玛格丽特微笑着说:“我就希望你问这个。”
真实世界用例 3:无限序列
“仔细看这个,”玛格丽特说,开始输入。“我们将创建一个永不结束的迭代器。”
“永不结束?”蒂莫西看起来有些担心。“那不会崩溃吗?”
“只要你小心使用,就不会。让我给你演示一下:”
class FibonacciIterator: """无限斐波那契序列"""def __init__(self): self.a, self.b = 0, 1 def __iter__(self): return self def __next__(self): value = self.a self.a, self.b = self.b, self.a + self.b return value这段代码定义了一个类的初始化方法、迭代器方法和下一个元素的方法。初始化方法将两个属性 `a` 和 `b` 分别设置为 0 和 1。迭代器方法返回自身,而下一个元素方法则返回当前的 `a` 值,并更新 `a` 和 `b` 的值,以便生成下一个 Fibonacci 数列的元素。
class Counter: """从 n 开始的无限计数器""" def __init__(self, start=0): self.current = start def __iter__(self): return self def __next__(self): value = self.current self.current += 1 return value def demonstrate_infinite_iterators(): """安全地展示无限序列"""print("前10个斐波那契数:") fib = FibonacciIterator() for i, num in enumerate(fib): print(num, end=' ') if i >= 9: # 在输出10个后停止 break print() print("n从100开始的计数器(前5个):") counter = Counter(100)for i, num in enumerate(counter): print(num, end=' ') if i >= 4: break print() print("n💡 无限迭代器 + break = 可控的无限序列!") demonstrate_infinite_iterators()输出:
前10个斐波那契数: 0 1 1 2 3 5 8 13 21 34 从100开始的计数器(前5个): 100 101 102 103 104 💡 无限迭代器 + break = 可控的无限序列!蒂莫西惊讶地盯着代码。“所以迭代器从不抛出StopIteration– 它只是不断运行下去。但我可以通过break或仅获取我需要的内容来控制何时停止。”“没错。无限迭代器出乎意料地有用,”玛格丽特说道。“ID 生成器、流处理器、事件循环 – 有很多现实世界的应用场景。关键是:迭代器协议并不要求你结束。你可以一直运行下去。”
“这真是让人震惊,”蒂莫西承认。“
iter()函数还可以做些什么?”“啊,”玛格丽特神秘地笑了笑。“有一个大多数人不知道的秘密第二种形式。”
iter() 函数的两种形式
玛格丽特调出了 Python 的文档。“你一直在使用的
iter()函数有一个非常有用但很少见的两参数形式。”“两个参数?”蒂莫西问道。
“看这里:”
def demonstrate_iter_with_sentinel(): """iter() 有一个两参数形式!""" # 形式 1:普通迭代器 # iter(iterable) → iterator # 形式 2:带哨兵的可调用对象# iter(callable, sentinel) → iterator # 重复调用 callable,直到返回 sentinel import random random.seed(42) def roll_die(): """模拟掷骰子""" return random.randint(1, 6) print("掷骰子直到得到 6:") # iter(callable, sentinel) - 调用 roll_die() 直到返回 6 for roll in iter(roll_die, 6):print(f" 投掷结果: {roll}") print(" 得到6!停止。n") # 另一个例子:读取文件块 def read_block(): """模拟从文件中读取块""" blocks = [b'data1', b'data2', b'', b'data3']return blocks.pop(0) if blocks else b'' print("读取块直到为空:") for block in iter(read_block, b''): print(f" 块: {block}") print(" 空块!已停止。") demonstrate_iter_with_sentinel()“真聪明!”提摩太说。“你不是从可迭代对象创建迭代器,而是从函数调用创建迭代器。它会不断调用这个函数,直到得到哨兵值。”
“没错。两参数形式是:
iter(callable, sentinel)。重复调用这个函数,直到它返回哨兵值,然后停止。”“我觉得这在文件读取或API轮询中会很有用,”提摩太沉思道。“不断调用,直到得到空响应或错误条件。”
“这些都是完美的用例,”玛格丽特确认道。“现在,有一件关键的事情你需要理解关于迭代器的内容——这是每个Python开发者在某个时刻都会遇到的问题。”
迭代器耗尽
“这里有一个最终会让你头疼的问题,”玛格丽特说,调出一个新的示例。“请仔细注意这一点。”
蒂莫西靠近,感觉这很重要。
def demonstrate_iterator_exhaustion(): """迭代器只能使用一次""" my_list = [1, 2, 3] # 列表是可迭代的(可以创建多个迭代器) print("列表(可迭代):") print(f" 第一个循环: {list(my_list)}")print(f" 第二次循环: {list(my_list)}") print(" ✓ 多次有效!n") # 迭代器耗尽 my_iterator = iter([1, 2, 3]) print("迭代器:") print(f" 第一次循环: {list(my_iterator)}")print(f" 第二次循环: {list(my_iterator)}") # 为空! print(" ✗ 迭代器在第一次使用后已耗尽!n") # 检查是否耗尽 my_iterator = iter([1, 2, 3]) print("检查耗尽状态:")print(f" 有项目: {next(my_iterator, 'EMPTY') != 'EMPTY'}") print(f" 下一个项目: {next(my_iterator)}") print(f" 下一个项目: {next(my_iterator)}")print(f" 下一个项目: {next(my_iterator, 'EMPTY')}") # 已耗尽 demonstrate_iterator_exhaustion()输出:
列表(可迭代): 第一次循环: [1, 2, 3] 第二次循环: [1, 2, 3] ✓ 多次工作! 迭代器:第一次循环: [1, 2, 3] 第二次循环: [] ✗ 迭代器在第一次使用后已耗尽! 检查耗尽情况: 是否有项目: 是 下一个项目: 2 下一个项目: 3 下一个项目: 空“哦!”蒂莫西惊呼,看到输出结果。“这个列表可以多次使用,因为每次都会创建一个新的迭代器。但迭代器本身只能使用一次——一旦耗尽,就结束了。”
“没错,”玛格丽特确认道。“这是最常见的迭代器错误。人们存储一个迭代器,使用一次后,就会想知道为什么第二次是空的。”
蒂莫西点头,做了个笔记。“所以如果我需要多次迭代,应该存储可迭代对象,而不是迭代器。可迭代对象可以根据需要创建新的迭代器。”
“完美的理解。现在让我向你展示Python内置的迭代器工具箱。”
内置迭代器工具
“Python有一个名为
itertools的模块,里面充满了有用的迭代器工具,”玛格丽特说,同时调出了文档。“这些工具让你以强大的方式组合迭代器。”“比如说什么?”蒂莫西问。
“让我给你展示一些经典示例:”
import itertools def demonstrate_iterator_tools(): """Python'的内置迭代器工具""" # itertools.count - 无限计数器 print("itertools.count(前5个):")for i, num in enumerate(itertools.count(10, 2)): print(num, end=' ') if i >= 4: break print() # itertools.cycle - 无限重复序列 print("nitertools.cycle(前10个):")for i, item in enumerate(itertools.cycle(['A', 'B', 'C'])): print(item, end=' ') if i >= 9: break print() # itertools.chain - 合并可迭代对象 print("nitertools.chain:")combined = itertools.chain([1, 2], ['a', 'b'], [10, 20]) print(f" {list(combined)}") # itertools.islice - 切片一个迭代器 print("nitertools.islice (从无限计数器中获取第2到第5项):")print(f" {list(itertools.islice(itertools.count(), 2, 6))}") # zip - 同时迭代多个序列 print("nzip:") names = ['Alice', 'Bob', 'Charlie'] ages = [25, 30, 35]for name, age in zip(names, ages): print(f" {name}: {age}") # enumerate - 添加索引 print("nenumerate:")for i, fruit in enumerate(['apple', 'banana', 'cherry'], start=1): print(f" {i}. {fruit}") demonstrate_iterator_tools()输出:
itertools.count (前5个): 10 12 14 16 18 itertools.cycle (前10个): A B C A B C A B C A itertools.chain: [1, 2, 'a', 'b', 10, 20] itertools.islice (从无限计数器中获取第2到第5个项): [2, 3, 4, 5] zip: Alice: 25 Bob: 30 Charlie: 35 enumerate: 1. apple 2. banana 3. cherry蒂莫西感到很惊讶。“这些就像积木一样。count提供无限序列,cycle重复,chain组合,islice在不将所有内容加载到内存中的情况下切片一个迭代器。你可以组合这些来解决复杂的问题。”“没错,”玛格丽特说。“而且它们都是惰性计算——在你实际迭代之前,什么都不会被计算。现在,让我们确保你理解我们一直在讨论的基本区别。”
迭代器与可迭代对象:关键区别
“我想彻底搞清楚这一点,”玛格丽特说,开始进行比较。“可迭代对象和迭代器之间有什么区别?”
蒂莫西思考了一会儿。“可迭代对象……可以被迭代。迭代器……执行迭代?”
“接近了。让我给你展示技术上的区别:”
def demonstrate_iterator_vs_iterable(): """理解关键区别""" # 列表是可迭代对象(不是迭代器)my_list = [1, 2, 3] print("列表(可迭代):") print(f" 是否具有 __iter__: {hasattr(my_list, '__iter__')}") print(f" 是否具有 __next__: {hasattr(my_list, '__next__')}")print(f" 是否是自己的迭代器: {iter(my_list) is my_list}") # 从可迭代对象获取迭代器 my_iterator = iter(my_list) print("n从列表获取的迭代器:") print(f" 是否具有 __iter__: {hasattr(my_iterator, '__iter__')}")print(f" 是否具有 __next__: {hasattr(my_iterator, '__next__')}") print(f" 是否是自己的迭代器: {iter(my_iterator) is my_iterator}") print("n关键区别:") print(" 可迭代对象: 可以创建迭代器 (__iter__)") print(" 迭代器: 执行迭代 (__next__)")print(" 迭代器也是可迭代的(从 __iter__ 返回自身)") demonstrate_iterator_vs_iterable()输出:列表(可迭代): 有 __iter__: True 有 __next__: False 是自己的迭代器:False 来自列表的迭代器: 有 __iter__: True 有 __next__: True 是自己的迭代器:True 关键区别: 可迭代:可以创建迭代器(__iter__) 迭代器:执行迭代(__next__) 迭代器也是可迭代的(从 __iter__ 返回自身)Pythonic捷径:生成器的概览
蒂莫西看着他们写的所有迭代器代码。“这很强大,但编写
__iter__和__next__并用self.current跟踪状态——这对于如此常见的东西来说,感觉像是很多样板代码。”“你说得完全正确,”玛格丽特带着会心的微笑说道。“Python的设计者也有同样的想法。这就是他们创建生成器的原因——使用
yield关键字创建迭代器的捷径。”“Yield?”蒂莫西问。
“看看这个。”玛格丽特打开一个新窗口并输入:
def countdown_generator(n): """生成器版本 - 简单得多!""" while n > 0: yield n n -= 1 # 和我们的迭代器完全一样使用 for num in countdown_generator(5): print(num, end=' ') print() # 可以多次使用 for num in countdown_generator(3):print(num, end=' ')输出:5 4 3 2 13 2 1蒂莫西盯着看。“就这样?只用
yield而不是所有那些__iter__和__next__的代码?”“就是这样,”玛格丽特确认道。“当你使用
yield时,Python 会自动为你创建一个迭代器。它处理__iter__、__next__、状态保存以及引发StopIteration。我们刚刚学习的关于迭代器的所有内容——Python 都为你处理了。”“那么如果生成器更简单,我们为什么要学习所有那些迭代器协议的东西?”
玛格丽特靠在椅子上。“因为理解迭代器协议让你明白 Python 的迭代实际上是如何工作的。生成器看起来很神奇,直到你意识到它们只是迭代器协议的语法糖。现在你既理解了机制,也理解了便利。”
她拉出一个比较:
# 迭代器类 - 显式协议 class CountDown:def __init__(self, start): self.start = start def __iter__(self): return CountDownIterator(self.start) class CountDownIterator: def __init__(self, start): self.current = start def __iter__(self): return self def __next__(self):if self.current <= 0: raise StopIteration value = self.current self.current -= 1 return value # 生成器 - 协议自动处理 def countdown_generator(start): while start > 0: yield start start -= 1 # 两者的工作方式完全相同 print("迭代器:", list(CountDown(3)))print("生成器:", list(countdown_generator(3)))输出:
迭代器: [3, 2, 1] 生成器: [3, 2, 1]“它们产生的结果完全相同,”蒂莫西观察到。“生成器只是……更简洁。”
“更简洁得多。这就是为什么生成器在实践中是创建自定义迭代器的 Pythonic 方式。但现在你明白了 为什么 它们有效——它们在内部实现了迭代器协议。”
玛格丽特站了起来。“在我们下次的对话中,我们将深入探讨生成器——它们是如何工作的,为什么它们在内存上高效,以及你可以用它们做的所有强大事情。但首先,让我给你展示一些在使用迭代器时需要避免的常见错误。”
常见陷阱
“在你去把所有东西都变成迭代器之前,”玛格丽特以警告的语气说道,“让我告诉你那些让每个人都陷入困境的陷阱。”
蒂莫西拿出了他的笔记本。“来吧,告诉我那些注意事项。”
“第一个是经典,”玛格丽特说,正在输入:
def pitfall_1_modifying_while_iterating(): """陷阱:在迭代时修改列表""" # ❌ 错误 - 在迭代过程中修改 items = [1, 2, 3, 4, 5] print("尝试移除偶数:") try: for item in items: if item % 2 == 0:items.remove(item) # 在迭代过程中进行修改!可能会跳过项目或引发 RuntimeError print(f" 结果: {items}") print(" ✗ 错过了项目 4!(迭代器混淆了)") except RuntimeError as e: print(f" ✗ RuntimeError: {e}") # ✓ 正确 - 遍历副本items = [1, 2, 3, 4, 5] print("n迭代副本:") for item in items[:]: # 切片创建副本 if item % 2 == 0: items.remove(item) print(f" 结果: {items}")print(" ✓ 正确!") # ✓ 正确 - 列表推导 items = [1, 2, 3, 4, 5] print("n列表推导:") items = [item for item in items if item % 2 != 0] print(f" 结果: {items}")print(" ✓ 最佳方法!") def pitfall_2_iterator_consumed(): """陷阱:重复使用迭代器""" iterator = iter([1, 2, 3]) print("n第一次消费:") result1 = list(iterator) print(f" {result1}") print("n第二次消费:")result2 = list(iterator) print(f" {result2}") print(" ✗ 迭代器已经耗尽!") def pitfall_3_infinite_without_break(): """陷阱:无限迭代器没有中断""" print("n无限迭代器需要中断:") print(" for i in itertools.count():") print(" if i > 5: break # ← 必须有停止条件!")pitfall_1_modifying_while_iterating() pitfall_2_iterator_consumed() pitfall_3_infinite_without_break()蒂莫西复习了他的笔记。“所以主要有三点:不要修改正在迭代的内容,记住迭代器会耗尽,并且对于无限迭代器始终要有停止条件。”
玛格丽特确认道:“这三点就能为你节省数小时的调试时间。现在让我来教你如何正确测试迭代器的行为。”
测试迭代器行为
“当你编写自定义迭代器时,”玛格丽特说,拉起一个测试文件,“你需要验证它们是否正确遵循协议。”
“我应该测试什么?”蒂莫西问。
“让我给你展示一些基本测试:”
import pytest def test_custom_iterator(): """测试自定义迭代器""" class SimpleIterator: def __init__(self, data): self.data = data self.index = 0 def __iter__(self): return selfdef __next__(self): if self.index >= len(self.data): raise StopIteration value = self.data[self.index] self.index += 1 return value iterator = SimpleIterator([1, 2, 3]) # 测试迭代assert next(iterator) == 1 assert next(iterator) == 2 assert next(iterator) == 3 # 测试 StopIteration with pytest.raises(StopIteration): next(iterator) def test_iterable_reusable(): """测试可迭代对象可以被多次迭代""" class Reusable: def __init__(self, data):self.data = data def __iter__(self): return iter(self.data) obj = Reusable([1, 2, 3]) # 第一次迭代 result1 = list(obj) assert result1 == [1, 2, 3] # 第二次迭代(应该可以工作!) result2 = list(obj)assert result2 == [1, 2, 3] def test_iterator_exhaustion(): """测试迭代器是否耗尽""" iterator = iter([1, 2, 3]) # 耗尽迭代器 list(iterator) # 现在应该是空的 assert list(iterator) == [] # 使用命令:pytest test_iterators.py -v图书馆隐喻
玛格丽特把它带回了图书馆:
“把迭代想象成从目录中借书,”她说。
“目录本身(一个可迭代对象)并不是借书的过程——它是一个使得借书成为可能的东西。当你开始借书时,你会得到一个书签(一个迭代器),它跟踪你在目录中的进度。
“书签有两个任务:
- 返回目录中的下一本书
- 记住你的位置
“你可以在同一个目录中拥有多个书签(一个可迭代对象的多个迭代器),每个书签独立跟踪其位置。一旦书签到达末尾,它就不能被重用——你需要一个新的书签才能再次浏览目录。
“这就是为什么
for循环可以与如此多的类型一起工作。每种类型都实现了相同的‘目录和书签’协议:提供一个书签(__iter__),而书签知道如何向前移动(__next__)。”关键要点
玛格丽特总结道:
""" 迭代器协议关键要点: 1. 两个关键方法: - __iter__(): 返回一个迭代器 - __next__(): 返回下一个项目或引发 StopIteration 2. 可迭代对象与迭代器: - 可迭代对象:可以创建迭代器(具有 __iter__) - 迭代器:执行迭代(具有 __next__ 和 __iter__) - 迭代器的 __iter__ 应返回自身 3. for 循环使用此协议: - for item in obj: → iter(obj) 然后重复调用 next() - 适用于任何实现该协议的对象 4. 关注点分离: - 可迭代对象:容器/序列 - 迭代器:迭代状态 - 允许多个同时迭代 5. 迭代器耗尽: - 迭代器只能使用一次 - 可迭代对象可以创建新的迭代器 - 存储可迭代对象,而不是迭代器 6. 生成器:Pythonic 的快捷方式: - 使用 'yield' 代替 __iter__ 和 __next__ - Python 自动处理协议 """7. 现实世界的应用: - 自定义集合 - 分页(懒加载) - 无限序列 - 流数据 - 文件处理 8. 内置工具: - itertools:count, cycle, chain, islice等 - zip:组合序列 - enumerate:添加索引 - iter(callable, sentinel):高级形式 9. 常见陷阱: - 在迭代时修改(可能跳过项目或引发RuntimeError) - 重用已耗尽的迭代器 - 无限迭代器没有中断 - 将可迭代对象与迭代器混淆 10. 优势: - 内存高效(一次一个项目) - 懒惰求值(按需计算) - 不同类型的统一接口 - 使强大的组合成为可能 11. 何时使用: - 自定义集合 - 大数据集(不要一次性加载所有) - 无限序列 - API分页 - 文件/流处理
蒂莫西点了点头,表示理解。“所以迭代器协议就是为什么
for循环如此通用的原因。每种类型都使用相同的语言——__iter__用于开始,__next__用于继续,StopIteration用于结束。而且我可以让我的自定义类型也讲这种语言!”“没错,”玛格丽特说。“迭代器协议是Python最优雅的设计之一。两个简单的方法,突然间任何对象都可以与
for循环、列表推导式、next()、zip()以及所有迭代器工具一起工作。”“而生成器,”蒂莫西补充道,“只是实现这个协议的一种方便方式,不需要所有的样板代码。”
“没错。这就是为什么我们接下来的讨论将完全围绕生成器——它们是如何在底层工作的,为什么它们如此节省内存,以及你可以用它们构建的所有强大模式。但现在你理解了基础:使这一切成为可能的迭代器协议。”
有了这些知识,蒂莫西能够创建自定义可迭代对象,理解迭代的真实工作原理,识别整个Python中协议的实际应用,并欣赏生成器为何是如此强大的特性——因为它们自动化了他现在深刻理解的协议。
Aaron Rose是一名软件工程师和tech-reader.blog的技术作家,同时也是Think Like a Genius的作者。
-
作者帖子
- 哎呀,回复话题必需登录。