迭代器与可迭代对象

列表、元组、字典能用 for 循环遍历,因为它们实现了 __iter__,是可迭代对象。迭代器则是实现了 __next__ 的对象,一次吐一个值。两者关系:可迭代对象可以被 iter() 变成迭代器。

很多人搞不清 for 循环背后发生了什么。其实 for 循环内部就是先调用 iter() 拿到迭代器,然后不断调 __next__() 取值,直到抛出 StopIteration 结束。理解了这一点,就理解了为什么文件、生成器、range 这些「不是列表」的东西也能被遍历。

nums = [1, 2, 3]
it = iter(nums)          # 拿到迭代器
print(next(it))          # 1
print(next(it))          # 2
print(next(it))          # 3
# next(it) 再调用就抛 StopIteration

自定义一个可迭代对象,实现 __iter__ 和 __next__ 两个方法就行。但日常开发很少手写迭代器类,因为有生成器这个更简洁的写法。

能 for 循环的不一定是列表,能 next() 的才是迭代器。
本文核心知识点
  • yield定义生成器的关键字。
  • 惰性边取边算,不占内存。
  • itertools无限迭代与组合工具。
  • send生成器双向通信。
Python 生成器配图:蓝调渐变背景上齿轮与管道中流动发光液体的扁平科技插画
数据像流水一样,边流边处理,这就是生成器。

生成器:yield 的魔法

函数里出现 yield 关键字,这个函数就变成生成器函数。调用它不会立刻执行函数体,而是返回一个生成器对象。只有 for 循环或 next() 开始消费时,函数体才真正执行,执行到 yield 就暂停,把值交出去,下次继续从暂停点往下走。

def count_down(n):
    while n > 0:
        yield n
        n -= 1

for x in count_down(3):
    print(x)    # 3, 2, 1

这种「暂停-恢复」机制就是协程的雏形。生成器把函数从「一次性算完」变成「按需分段算」,是 Python 惰性求值的核心实现。

惰性求值与内存优势

处理上亿条记录时,一次性读进列表内存直接爆掉。生成器一次只保留当前一条,内存占用是常量级的。

# 危险:一次性生成 1000 万个数的列表
big_list = [x for x in range(10_000_000)]
# big_list 占约 80MB 内存

# 安全:生成器边取边算
big_gen = (x for x in range(10_000_000))
# 生成器对象本身只有几十字节

# 读大文件逐行处理
def read_lines(path):
    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            yield line.strip()

for line in read_lines("huge.log"):
    process(line)   # 内存永远只占一行

生成器表达式(圆括号的推导式)和列表推导式(方括号)区别就在这:前者惰性,后者立即求值。大数据场景优先用生成器表达式。

itertools 工具箱

标准库 itertools 提供一批处理迭代器的工具,全是惰性的,组合起来能做很多优雅的事。

from itertools import islice, chain, cycle, product, groupby

# islice:切片迭代器(像列表切片,但惰性)
first_10 = islice(counter(), 10)

# chain:串联多个迭代器
for x in chain([1, 2], ["a", "b"]):
    pass    # 1, 2, "a", "b"

# cycle:无限循环
for x in islice(cycle(["红", "绿", "蓝"]), 5):
    pass    # 红, 绿, 蓝, 红, 绿

# product:笛卡尔积
for combo in product([1, 2], ["a", "b"]):
    pass    # (1,a) (1,b) (2,a) (2,b)

# groupby:相邻分组(先排序再分组)
for key, group in groupby(sorted(data, key=lambda d: d["city"]), lambda d: d["city"]):
    print(key, list(group))

itertools 还有 takewhile、dropwhile、accumulate、zip_longest 等一批工具,写数据处理脚本时多翻翻标准库文档,往往比手写循环更简洁高效。

send 与生成器协程

生成器不仅能向外吐值,还能通过 send() 接收外部传进来的值,实现双向通信。这被称作「生成器协程」。

def accumulator():
    total = 0
    while True:
        value = yield total   # 先吐当前和,再收新值
        if value is None:
            break
        total += value

gen = accumulator()
next(gen)          # 启动,停在 yield
print(gen.send(10))   # 10
print(gen.send(5))    # 15
gen.close()

send 之前必须先 next() 启动一次。这种模式可以做成简易流水线:上游 send 数据,下游 yield 结果。虽然 asyncio 提供了更完善的协程方案,但理解生成器协程对读懂老代码和装饰器很有帮助。