迭代器与可迭代对象
列表、元组、字典能用 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生成器双向通信。
生成器: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 提供了更完善的协程方案,但理解生成器协程对读懂老代码和装饰器很有帮助。
常见坑与性能注意
| 坑 | 后果 | 应对 |
|---|---|---|
| 生成器被多次遍历 | 第二次遍历是空的 | 需要复用就 list() 转列表 |
| 递归里 yield 忘了 yield from | 只吐生成器对象 | 用 yield from 委托子生成器 |
| 大列表推导代替生成器 | 内存暴涨 | 大数据用生成器表达式 |
| 生成器长期不关闭 | 文件句柄泄漏 | 配合 contextlib.closing 或 with |
# yield from 委托子生成器
def outer():
yield from inner_gen()
yield "done"
生成器只能遍历一次,需要复用就物化成列表。
生成器实战清单
- 大文件、大数据流用生成器逐条处理。
- 生成器表达式比列表推导省内存。
- itertools 工具让数据处理更简洁。
- send 实现双向通信,理解协程雏形。
- yield from 委托子生成器避免嵌套循环。
学会生成器,Python 的数据处理才算真正入门。