Python 生成器与 yield:处理大文件不爆内存
2026/7/13 11:23:07 网站建设 项目流程

Python 生成器与 yield:处理大文件不爆内存

处理一个几 GB 的日志文件,新手常写data = f.readlines()content = f.read(),然后程序直接MemoryError崩了,或者机器卡到风扇狂转。原因很简单:这两种写法都把整个文件一次性塞进内存。

Python 的生成器(generator)就是解决这类问题的利器——它让你「用多少读多少」,内存占用和文件大小几乎无关。这篇从大文件读取这个具体痛点讲起,把yield和生成器讲透。

问题:一次性读取直接爆内存

# 反例:2GB 文件直接把内存打满defcount_errors(path):withopen(path)asf:lines=f.readlines()# 整个文件读进 list,2GB 文件就吃 2GB+ 内存returnsum(1forlineinlinesif"ERROR"inline)

readlines()返回一个包含所有行的列表,文件多大内存就吃多大。同理f.read()把整个内容读成一个字符串,一样爆。

生成器基础:yield 到底做了什么

普通函数用return一次性返回结果就结束了。带yield的函数叫生成器函数,调用它不会立即执行,而是返回一个生成器对象;每次向它要一个值(next()for迭代),函数才执行到下一个yield,吐出一个值后暂停,保留全部局部状态,下次接着往下跑。

defcounter(n):print("开始")foriinrange(n):yieldi# 吐出 i,然后暂停在这里print("结束")gen=counter(3)# 此时函数体一行都没执行print(next(gen))# 打印「开始」,再打印 0print(next(gen))# 打印 1(从上次暂停处继续)print(next(gen))# 打印 2# print(next(gen)) # 再要会打印「结束」并抛 StopIteration

关键点:生成器是「惰性」的,值是按需生产的,任何时刻内存里只有一个值,不是全部值。这正是它省内存的原因。

用生成器逐行处理大文件

其实 Python 的文件对象本身就是生成器——直接for line in f就是逐行惰性读取,根本不用readlines():

defcount_errors(path):withopen(path)asf:# for line in f 逐行读,内存里始终只有当前一行returnsum(1forlineinfif"ERROR"inline)

这个版本处理 2GB 还是 200GB,内存占用都是常数级(一行的大小)。

如果读的不是按行分隔,而是要按固定大小分块(比如二进制文件),自己写个生成器:

defread_chunks(path,chunk_size=1024*1024):"""按 1MB 分块惰性读取,适合大二进制文件"""withopen(path,"rb")asf:whileTrue:chunk=f.read(chunk_size)ifnotchunk:# 读到文件末尾breakyieldchunk# 吐出一块,暂停,等下次要# 用法:算一个大文件的 MD5,内存只占 1MBimporthashlibdeffile_md5(path):h=hashlib.md5()forchunkinread_chunks(path):h.update(chunk)returnh.hexdigest()

进阶:生成器管道,像流水线一样处理数据

生成器真正强大的地方,是能串成管道——每一级只处理一条数据就传给下一级,全程不落地成大列表。这和 Unix 的cat | grep | awk是一个思路。

defread_lines(path):withopen(path)asf:forlineinf:yieldline.rstrip("\n")defonly_errors(lines):forlineinlines:if"ERROR"inline:yieldlinedefextract_ip(lines):forlineinlines:# 假设每行以「IP 空格」开头yieldline.split(" ",1)[0]# 把三级串起来:数据一条条流过,内存始终只有一行pipeline=extract_ip(only_errors(read_lines("app.log")))foripinpipeline:print(ip)

注意:整个pipelinefor循环真正迭代之前,一行都没读。这就是惰性求值的威力——你搭好流水线,数据才在你要的时候一条条流过。

一个容易踩的坑:生成器只能消费一次

生成器是一次性的,迭代完就空了,再迭代什么都没有:

gen=(x*xforxinrange(3))# 生成器表达式print(list(gen))# [0, 1, 4]print(list(gen))# [] <- 已经耗尽,不会报错但为空

如果你需要多次遍历,要么每次重新创建生成器,要么老老实实用list存下来(但那就放弃了省内存的好处,权衡清楚)。另外,别对生成器用len()——它不知道自己有多少个元素,会直接TypeError

顺带一提,上面(x * x for x in range(3))这种生成器表达式(圆括号)和列表推导(方括号[...])长得像,但一个惰性省内存、一个立即算出整个列表。处理大数据时优先用圆括号版本。

小结

  • 大文件别用f.read()/f.readlines(),会一次性吃满内存。
  • yield让函数变成生成器:惰性、按需生产,任意时刻内存里只有一个值。
  • 逐行读直接for line in f;按块读自己写while + f.read(size) + yield的生成器。
  • 生成器可以串成管道,数据像流水线一样一条条流过,全程不落地,直到真正迭代才开始读。
  • 坑:生成器只能消费一次,耗尽后为空;不能对它用len();需要多次遍历就重建或转 list。

一句话记忆:return是「一次给完」,yield是「要一个给一个」——处理大数据时,惰性才是省内存的关键。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询