蒂莫西正在向一位来自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('line1\nline2\nline3')
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 StopIteration
value = 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("\n\n第二个循环:")
for num in countdown:
print(num, end=' ') # 这会有效吗?
输出:
第一次循环:
3 2 1
第二次循环:
“第二次循环什么都没有!”蒂莫西惊呼道。“为什么?”
“因为迭代器在第一次循环中耗尽了。current 的值现在是 0,并且保持为 0。如果你想再次迭代,你需要一个新的迭代器。这就是我们需要区分 可迭代对象 和 迭代器 的地方。”
区分可迭代对象和迭代器
玛格丽特解释了一个重要的区别:
"""
最佳实践:区分可迭代对象和迭代器
可迭代对象:可以被迭代的容器
迭代器:实际进行迭代的对象
这允许:
- 多个同时迭代
- 重用可迭代对象
- 更清晰的设计
"""
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 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('line1\nline2')
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_end
return 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 1 3 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 self
def __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的作者。

