首页 论坛 置顶 Python的秘密生活:迭代器协议 – 为什么for循环如此神奇

正在查看 1 个帖子:1-1 (共 1 个帖子)
  • 作者
    帖子
  • #27334

    蒂莫西正在向一位来自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 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("nn第二个循环:")
    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('line1nline2')
    
    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的作者。

正在查看 1 个帖子:1-1 (共 1 个帖子)
  • 哎呀,回复话题必需登录。