openpyxl 写 Excel 的三个隐蔽坑:等号开头的文本、列宽自适应、公式缓存
Python 生成器实战:用惰性求值把内存从 O(n) 降到 O(1)
处理大文件、大接口分页、无限流数据时,很多人第一反应是把数据全部读进 list 再处理。数据量一大,内存立刻爆炸。生成器(generator)是 Python 里解决这类问题的标准答案,但实际用起来有几个隐蔽的坑,这篇按实战顺序讲清楚。
一、惰性求值的核心:一次只产出一项
生成器函数用 yield 返回数据,调用它得到的是生成器对象,函数体并不执行,直到每次 next() 推进到下一个 yield:
def read_large(path):
with open(path, encoding='utf-8') as f:
for line in f:
line = line.strip()
if line:
yield line
logs = read_large('access.log') # 此刻什么都没读
first = next(logs) # 只读第一行
同样的文件如果写 lines = f.readlines(),1GB 的日志就要吃掉数倍内存;生成器版本任何时刻只持有一行。
二、坑一:生成器是一次性的
生成器耗尽后再迭代,得到的是空结果,而且不报错——这是最容易踩的静默 bug:
errors = (l for l in read_large('access.log') if 'ERROR' in l)
print(sum(1 for _ in errors)) # 1203
print(sum(1 for _ in errors)) # 0 !
对策只有两个:要么需要多次使用就改成列表(接受内存代价),要么把生成器封装成工厂函数,每次调用产生新的生成器。
三、坑二:生成器表达式引用外层变量是惰性绑定
keys = ['a', 'b', 'c']
gens = [k for k in keys] # 没问题
funcs = [lambda: k for k in keys] # 三个 lambda 全返回 'c'
lambda 捕获的是变量 k 本身而不是当时的值,循环结束后 k 停在 'c'。生成器表达式同理——如果生成器内部引用了循环变量,真正 next() 时才求值。修法是给函数参数做默认值绑定:lambda k=k: k。
四、坑三:yield 与 return 的合作
生成器里 return 不是返回值,而是触发 StopIteration;只有 yield from 委托链才能把返回值带出来(PEP 380),日常代码里几乎用不到,但调试看到 StopIteration('xxx') 携带值时要知道这是 return 的产物,不是异常数据。
五、实战模式:分页拉取转惰性流
把"循环翻页拉接口"封装成生成器,调用方就能用统一的 for 循环处理,翻页细节全部隐藏:
def iter_pages(fetch, page_size=100):
page = 1
while True:
items = fetch(page, page_size)
if not items:
return
yield from items
page += 1
total = sum(1 for _ in iter_pages(fetch_orders)) # 内存恒定
配合 itertools.islice 还能随时截断:islice(iter_pages(fetch), 500) 只拉前 500 条就停止翻页,请求数也随之减少——这是 list 方案做不到的。
小结
| 场景 | 用法 |
|---|---|
| 逐行处理大文件 | yield line,内存 O(1) |
| 多次消费 | 工厂函数每次新建,勿复用耗尽的生成器 |
| 循环里建 lambda/生成器 | 默认参数绑定 k=k |
| 接口分页 | yield from items + islice 截断 |
生成器不是语法糖,是把"存储整份数据"换成"按需生产数据"的结构性手段。写数据管道时先想清楚数据会被消费几次,再决定生成器还是列表。
- 点赞
- 收藏
- 关注作者
评论(0)