Python 生成器实战:用 yield 逐行处理大日志文件而不爆内存
背景
排查线上问题时,经常要从几百 MB 的日志文件里找出报错行。最直观的写法是 readlines() 一次性读入全部内容,再用列表推导过滤。文件不大时没有问题,文件一大,内存占用会跟着文件体积线性增长,轻则变慢,重则被系统杀掉进程。
生成器是 Python 处理这类流式数据的标准工具:它一次只产生一个值,用完即丢,内存占用基本恒定。
实现思路
核心是把“读一行、判断一行、产出一行”写成生成器函数,调用方按需消费,中间不产生大列表。
逐行读取并过滤
def error_lines(path):
"""逐行产出包含 ERROR 的日志行,内存占用与文件大小无关。"""
with open(path, encoding="utf-8") as f:
for line in f:
if "ERROR" in line:
yield line.rstrip("\n")
文件对象本身就是可迭代对象,for line in f 每次只读一行,配合 yield 后函数会在每次产出后暂停,调用方取走一行才继续。
用生成器表达式做惰性统计
不需要把结果存下来时,生成器表达式比列表推导更省内存:
error_count = sum(1 for line in error_lines("app.log"))
sum 一边消费一边累加,全程只保留一个计数器。
组合多个生成器做流水线
过滤、转换可以拆成多个小生成器串联,每个环节都是惰性的:
from itertools import islice
def timestamps(lines):
for line in lines:
yield " ".join(line.split(" ")[:2])
first_errors = list(islice(timestamps(error_lines("app.log")), 5))
只有最后的 list(...) 会把结果物化,而且这里只取了前 5 条。注意生成器不能像列表那样切片,取前 N 条要用 itertools.islice。
验证结果
本地用 Python 3.12 实测:先生成一个 20 万行、约 9.7 MB 的测试日志,其中混入 1000 行 ERROR。
import tracemalloc
tracemalloc.start()
count = sum(1 for _ in error_lines("test.log"))
current, peak = tracemalloc.get_traced_memory()
print(count, f"峰值内存 {peak / 1024:.1f} KB")
tracemalloc.stop()
实测输出:1000 峰值内存 22.2 KB。作为对照,readlines() 读入同一文件再过滤,计数同样是 1000,峰值内存约为 18.6 MB,与文件体积相当。
结论:生成器版本的峰值内存不随文件行数增长,只与单行长度有关;readlines() 版本的峰值内存与文件体积成正比。
注意事项
- 生成器只能消费一次:
sum(...)用完后,同一个生成器对象不能再遍历,需要重新调用函数生成新的。 yield之后的代码在下一次next()时才继续执行,打开的文件句柄要放在with块里,确保消费结束或中途退出时正确关闭。- 如果中途
break退出循环,生成器会被垃圾回收关闭,未读完的文件句柄同样会被with块释放,不需要手动处理。 - 需要随机访问或多次遍历时,生成器不合适,应改用列表或把结果写入临时文件。
- 点赞
- 收藏
- 关注作者
评论(0)