迭代器 & 生成器:把 Python 的“惰性魔法”用到极致 #Day10
·
大家好,我是 亦难。
今天我们把「迭代器」与「生成器」这两兄弟拆开揉碎,再用一条故事线串起来:
“如何优雅地读完一个 10 GB 的日志文件,而内存只占 20 MB?”
答案就在本文里。整篇阅读约 8 分钟,代码量控制在“一眼能看明白”的范围内。
一、为什么需要“惰性”?
想象你在视频网站刷剧:
- 一次性加载全集 → 浏览器瞬间卡死(相当于把 10 GB 日志一次性读进内存)。
- 点一集看一集 → 永远流畅(这就是惰性迭代)。
迭代器和生成器就是 Python 给你的“点一集看一集”的官方实现。
二、迭代器:最老实的搬运工
1️⃣ 定义与协议
迭代器必须同时满足两个魔法方法:
__iter__():返回自身。__next__():返回下一个元素,没有就抛StopIteration。
2️⃣ 手写一个“倒计时迭代器”
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current < 0:
raise StopIteration
val = self.current
self.current -= 1
return val
# 使用
for num in Countdown(5):
print(num, end=" ") # 5 4 3 2 1 0
注意:迭代器是一次性用品,跑完一轮就“报废”。
三、生成器:语法糖里的黑科技
1️⃣ 生成器推导式
把列表推导式的 [] 换成 (),立刻变惰性:
gen = (i * 2 for i in range(5))
print(gen) # <generator object ...>
print(next(gen)) # 0
print(list(gen)) # [2, 4, 6, 8]
2️⃣ yield 关键字
在普通函数里写 yield,它会在每次调用时冻结现场,下次从断点继续。
def fib(n):
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b
for f in fib(6):
print(f, end=" ") # 0 1 1 2 3 5
四、迭代器 vs 生成器:一张图看懂区别
| 维度 | 迭代器 | 生成器 |
|---|---|---|
| 实现方式 | 手写 __iter__ & __next__ |
函数里写 yield |
| 代码量 | 多 | 少 |
| 状态保存 | 手动 | 自动 |
| 内存占用 | 取决于实现 | 极低 |
| 典型用途 | 高度定制 | 快速惰性序列 |
五、实战:10 GB 日志文件的“零拷贝”读取
需求:统计日志里出现次数最多的 IP。
传统做法:readlines() 会把 10 GB 全部塞进内存 → 直接爆炸。
生成器做法:一行一行吐出来,内存恒定在几十 MB。
def read_lines(path):
with open(path, 'r', encoding='utf-8') as f:
for line in f:
yield line
# 使用
ip_counter = {}
for line in read_lines('huge.log'):
ip = line.split()[0]
ip_counter[ip] = ip_counter.get(ip, 0) + 1
print(ip_counter.most_common(10))
六、常见误区 & 面试金句
- “迭代器只能遍历一次” —— 真;想再遍历请重新实例化。
- “生成器一定是迭代器,迭代器不一定是生成器” —— 真;
yield会自动实现迭代器协议。 - “惰性就一定快?” —— 不一定;惰性解决的是内存问题,不是CPU问题。
七、总结口诀
- 迭代器:自己写,麻烦但灵活。
- 生成器:用
yield,简单且省内存。 - 何时用:数据量大、流式输入、管道式处理,优先考虑生成器。
✨ 明日预报
《时间与空间复杂度:用 O 记法给算法“称体重”》
明晚 ,带你用 15 分钟学会:
- 如何一眼看出
O(n²)与O(n log n)的差距 - 为什么二分查找永远比线性扫描“轻”
- 空间换时间的经典套路与避坑指南
点个收藏,明晚一起“称代码的重量”!
更多推荐
所有评论(0)