Python3 迭代器与生成器

在 Python 3 中,迭代器 (Iterator)生成器 (Generator) 是语言中最核心、最优雅的高级特性之一。它们是实现惰性求值 (Lazy Evaluation) 的基石,使得 Python 能够以极低的内存占用优雅地处理海量数据、无限序列以及复杂的数据流管道。

掌握这两者,不仅是理解 Python for 循环底层机制的关键,更是迈向高级 Python 开发(如异步编程、协程、大数据处理)的必经之路。

以下是对 Python 3 迭代器与生成器的全面、深度解析,涵盖概念辨析、底层原理、高级特性及最佳实践。(本文篇幅较长,旨在提供一份可作为生产环境参考手册的深度指南)


一、 核心概念辨析:Iterable vs Iterator

在深入代码之前,必须严格区分两个经常被混淆的概念:可迭代对象 (Iterable)迭代器 (Iterator)

1. 可迭代对象 (Iterable)

  • 定义:任何可以返回一个迭代器的对象。通俗地说,就是可以放在 for 循环 in 关键字后面的对象
  • 底层协议:实现了 __iter__() 方法的对象。调用该方法会返回一个迭代器。
  • 常见示例list, tuple, str, dict, set, range 对象,以及打开的文件对象。
  • 验证方法:使用 collections.abc.Iterable 进行判断。

2. 迭代器 (Iterator)

  • 定义:表示数据流的对象。它负责实际执行迭代过程,每次产出下一个值,直到没有数据为止。
  • 底层协议:必须同时实现两个方法:
    1. __iter__(): 返回迭代器自身(这使得迭代器本身也是可迭代的)。
    2. __next__(): 返回数据流中的下一个值。如果没有更多数据,必须抛出 StopIteration 异常。
  • 验证方法:使用 collections.abc.Iterator 进行判断。

3. 它们的关系

所有的迭代器都是可迭代对象,但并非所有的可迭代对象都是迭代器。

  • 列表 [1, 2, 3] 是 Iterable,但不是 Iterator(它没有 __next__ 方法)。
  • 通过 iter([1, 2, 3]) 获得的对象,既是 Iterator,也是 Iterable。
纯文本
from collections.abc import Iterable, Iterator

my_list = [1, 2, 3]
my_iter = iter(my_list)

print(isinstance(my_list, Iterable))   # True
print(isinstance(my_list, Iterator))   # False (列表本身不是迭代器)

print(isinstance(my_iter, Iterable))   # True
print(isinstance(my_iter, Iterator))   # True

二、 for 循环的底层真相

理解了上述概念,我们就能揭开 Python for 循环的神秘面纱。当你写下以下代码时:

纯文本
for item in [1, 2, 3]:
    print(item)

Python 解释器在底层实际执行的是以下等价代码:

纯文本
# 1. 获取可迭代对象的迭代器
iterator = iter([1, 2, 3])  # 调用 [1, 2, 3].__iter__()

while True:
    try:
        # 2. 不断调用 next() 获取下一个值
        item = next(iterator)  # 调用 iterator.__next__()
        # 3. 执行循环体
        print(item)
    except StopIteration:
        # 4. 捕获 StopIteration 异常,优雅地退出循环
        break

💡 核心启示for 循环本质上是一个 while True 加上 try...except StopIteration 的语法糖。


三、 自定义迭代器 (Custom Iterator)

虽然 Python 提供了丰富的内置迭代器,但了解如何通过类来手动实现迭代器协议,有助于深刻理解其机制。

示例:实现一个自定义的计数器迭代器

纯文本
class CounterIterator:
    def __init__(self, start, end):
        self.current = start
        self.end = end

    def __iter__(self):
        # 迭代器必须返回自身
        return self

    def __next__(self):
        if self.current < self.end:
            result = self.current
            self.current += 1
            return result
        else:
            # 数据耗尽时,必须抛出 StopIteration
            raise StopIteration

# 使用自定义迭代器
counter = CounterIterator(1, 4)
print(next(counter))  # 1
print(next(counter))  # 2
print(next(counter))  # 3
# print(next(counter))  # 抛出 StopIteration

# 也可以直接用 for 循环
for num in CounterIterator(1, 4):
    print(num)  # 依次输出 1, 2, 3

缺点:为了实现一个简单的序列,我们需要编写一个完整的类,管理内部状态 (self.current),代码显得冗长且笨重。这正是生成器诞生的原因。


四、 生成器 (Generator):迭代器的优雅升华 🌟

生成器是 Python 提供的一种更简洁、更强大的创建迭代器的方式。它不需要你手动编写类和维护 __iter__ / __next__ 方法,Python 解释器会自动为你处理所有状态保存和恢复的工作。

生成器有两种创建方式:生成器函数生成器表达式

1. 生成器函数 (Generator Function)

任何包含 yield 关键字的函数,都会自动变成生成器函数。调用它不会立即执行函数体,而是返回一个生成器对象(它本身就是一个迭代器)。

yield 的工作原理
当代码执行到 yield 时,函数会暂停 (Pause),并将 yield 后面的值返回给调用者。函数的所有局部变量和执行状态都会被保存。下次调用 next() 时,函数会从上次暂停的地方恢复 (Resume) 执行,直到遇到下一个 yield 或函数结束。

纯文本
def simple_generator():
    print("第一次调用 next(),执行到这里")
    yield 1
    print("第二次调用 next(),从这里恢复")
    yield 2
    print("第三次调用 next(),从这里恢复")
    yield 3
    print("函数执行完毕")

# 1. 调用生成器函数,获取生成器对象 (此时函数体未执行)
gen = simple_generator()
print(type(gen))  # <class 'generator'>

# 2. 驱动生成器
print(next(gen))  # 输出: "第一次调用..." 然后返回 1
print(next(gen))  # 输出: "第二次调用..." 然后返回 2
print(next(gen))  # 输出: "第三次调用..." 然后返回 3
# print(next(gen))  # 输出: "函数执行完毕",然后抛出 StopIteration

2. 用生成器重写斐波那契数列

对比之前自定义迭代器的冗长,生成器的简洁性展露无遗:

纯文本
def fibonacci(n):
    a, b = 0, 1
    for _ in range(n):
        yield a
        a, b = b, a + b  # 状态自动保存,下次从这里继续

# 生成前 10 个斐波那契数
for num in fibonacci(10):
    print(num, end=" ")  # 0 1 1 2 3 5 8 13 21 34

3. 生成器表达式 (Generator Expression)

我们在“推导式”章节已经简要提及。它是列表推导式的惰性版本,使用圆括号 ()

纯文本
# 列表推导式:立即计算,占用大量内存
list_comp = [x ** 2 for x in range(1000000)] 

# 生成器表达式:惰性计算,几乎不占内存
gen_exp = (x ** 2 for x in range(1000000))

print(next(gen_exp))  # 0
print(next(gen_exp))  # 1

五、 Python 3 生成器的高级特性

1. yield from (Python 3.3+)

当需要在一个生成器中委托(delegate)给另一个可迭代对象或子生成器时,yield from 是最佳选择。它会自动处理子生成器的 StopIteration,并可以传递返回值。

纯文本
def sub_generator():
    yield "A"
    yield "B"
    return "Sub-generator finished"  # Python 3.3+ 允许生成器返回值

def main_generator():
    yield 1
    # 委托给 sub_generator,并接收其返回值
    result = yield from sub_generator()
    yield 2
    print(f"Received from sub: {result}")
    yield 3

gen = main_generator()
print(next(gen))  # 1
print(next(gen))  # A
print(next(gen))  # B
print(next(gen))  # 2 (同时打印: Received from sub: Sub-generator finished)
print(next(gen))  # 3

应用场景:递归遍历目录树、展平嵌套列表、构建复杂的数据处理管道。

2. 生成器的双向通信:.send(), .throw(), .close()

生成器不仅仅是“产出”数据,它还可以“接收”数据。这使得生成器成为了 Python 协程 (Coroutine) 的基础(尽管现代 Python 更推荐使用 async/await)。

  • gen.send(value): 恢复生成器,并将 value 作为当前 yield 表达式的结果返回给生成器内部。
  • gen.throw(Exception): 在生成器暂停的位置抛出一个异常。
  • gen.close(): 在生成器暂停的位置注入 GeneratorExit 异常,用于清理资源。
纯文本
def echo_generator():
    print("Generator started")
    while True:
        # yield 表达式的值就是 send() 传入的值
        received = yield
        print(f"Echo: {received}")

gen = echo_generator()
next(gen)          # 启动生成器,执行到第一个 yield 暂停。输出: Generator started
gen.send("Hello")  # 恢复执行,"Hello" 赋值给 received。输出: Echo: Hello
gen.send("World")  # 输出: Echo: World
gen.close()        # 优雅关闭生成器

六、 底层原理与性能分析

1. 内存占用的降维打击

生成器的核心价值在于空间复杂度从 $O(N)$ 降为 $O(1)$。它不存储数据,只存储产生数据的算法和当前状态

纯文本
import sys

# 场景:处理 1 亿个数字的平方
N = 100_000_000

# 列表:尝试分配约 800MB+ 内存,可能导致 MemoryError
# large_list = [x ** 2 for x in range(N)] 

# 生成器:无论 N 多大,内存占用恒定在约 100 字节左右
large_gen = (x ** 2 for x in range(N))
print(f"Generator size: {sys.getsizeof(large_gen)} bytes") # ~104 bytes

2. 时间复杂度与延迟计算 (Lazy Evaluation)

生成器采用延迟计算。如果你只需要前 5 个结果,它绝不会计算第 6 个及以后的结果。这在处理无限序列(如实时日志流、传感器数据)或早期退出 (break) 的搜索场景中,能节省巨大的 CPU 时间。

3. 为什么生成器只能遍历一次?

迭代器的设计哲学是“单向向前的数据流”。一旦 __next__ 抛出了 StopIteration,该迭代器就宣告耗尽 (Exhausted)。再次对其调用 next() 或放入 for 循环,将立即抛出 StopIteration 或什么都不做。
(如果需要多次遍历,必须重新调用生成器函数或重新创建生成器表达式。)


七、 核心避坑指南与最佳实践

1. ❌ 陷阱一:试图对耗尽的生成器进行二次遍历

这是新手最常遇到的 Bug。

纯文本
gen = (x for x in range(3))

# 第一次遍历:正常
for item in gen:
    print(item)  # 0, 1, 2

# 第二次遍历:静默失败,什么都不输出!
for item in gen:
    print(item)  

✅ 解决方案:如果需要多次使用,要么将其转换为列表 list(gen)(牺牲内存),要么重新实例化生成器。

2. ❌ 陷阱二:在生成器中误用 return

在 Python 3.3+ 中,生成器允许使用 return value。但这不会像普通函数那样返回值给调用者,而是会立即触发 StopIteration(value) 异常,提前终止生成器。

纯文本
def bad_gen():
    yield 1
    return "Done"  # 这会触发 StopIteration("Done"),而不是返回字符串
    yield 2        # 这行永远不会执行

g = bad_gen()
print(next(g))     # 1
# print(next(g))   # 抛出 StopIteration: Done

最佳实践:在普通生成器中,尽量避免使用 return,除非你明确知道自己在做生成器委托 (yield from)。

3. ❌ 陷阱三:生成器表达式中的晚期绑定 (Late Binding)

这与闭包中的晚期绑定问题相同。如果在生成器表达式中引用了外部循环变量,可能会得到意想不到的结果。

纯文本
# ❌ 错误:所有生成器都引用了同一个变量 i,最终 i 的值是 2
generators = [(lambda: i) for i in range(3)]
print([g() for g in generators])  # [2, 2, 2]

# ✅ 正确:使用默认参数强制早期绑定,或在生成器内部立即求值
generators = [(lambda x=i: x) for i in range(3)]
print([g() for g in generators])  # [0, 1, 2]

4. 💡 最佳实践:何时必须使用生成器?

  1. 读取超大文件:逐行读取,而不是一次性 readlines() 加载到内存。
纯文本
   with open("huge_log.txt", "r") as f:
       for line in f:  # f 本身就是一个迭代器,每次只加载一行到内存
           process(line)
  1. 构建数据处理管道 (Pipeline):将多个生成器串联,数据像流水线一样流过,内存占用始终极低。
纯文本
   def read_data(filename):
       with open(filename) as f:
           for line in f:
               yield line.strip()

   def filter_errors(lines):
       for line in lines:
           if "ERROR" not in line:
               yield line

   def parse_to_dict(lines):
       for line in lines:
           yield {"raw": line}

   # 管道组装:此时没有任何数据被实际读取或处理
   pipeline = parse_to_dict(filter_errors(read_data("app.log")))

   # 驱动管道:数据开始流动,内存占用极小
   for record in pipeline:
       save_to_db(record)
  1. 表示无限序列:如持续生成的时间戳、随机数流等。

八、 总结

迭代器与生成器是 Python 语言设计中“优雅”与“实用”完美结合的典范。

  1. 协议层面__iter____next__ 构成了 Python 迭代协议的基石,统一了所有集合类型的遍历方式。
  2. 语法层面yield 关键字将复杂的类状态管理简化为直观的函数暂停与恢复,极大地提升了代码的可读性。
  3. 性能层面:惰性求值 (Lazy Evaluation) 使得 Python 能够以 $O(1)$ 的空间复杂度处理 $O(N)$ 甚至无限的数据流,这是 Python 能够胜任数据科学和后端高并发任务的关键能力之一。

当你开始习惯用生成器表达式代替列表推导式来处理大数据,用 yield 来构建数据处理管道时,你就真正掌握了 Python 内存管理与控制流的高级艺术。

如果你对生成器如何演变为现代 Python 的 async/await 异步协程,或者 itertools 模块中更复杂的迭代器组合工具有兴趣,我们可以继续深入探讨!