Python的秘密生活:迭代器协议 – 为什么for循环如此神奇

蒂莫西正在向一位来自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_iteratorstr_iteratordict_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的作者。

更多