
Python 生成器与 yield:处理大文件不爆内存处理一个几 GB 的日志文件,新手常写data f.readlines()或content f.read(),然后程序直接MemoryError崩了,或者机器卡到风扇狂转。原因很简单:这两种写法都把整个文件一次性塞进内存。Python 的生成器(generator)就是解决这类问题的利器——它让你「用多少读多少」,内存占用和文件大小几乎无关。这篇从大文件读取这个具体痛点讲起,把yield和生成器讲透。问题:一次性读取直接爆内存# 反例:2GB 文件直接把内存打满defcount_errors(path):withopen(path)asf:linesf.readlines()# 整个文件读进 list,2GB 文件就吃 2GB 内存returnsum(1forlineinlinesifERRORinline)readlines()返回一个包含所有行的列表,文件多大内存就吃多大。同理f.read()把整个内容读成一个字符串,一样爆。生成器基础:yield 到底做了什么普通函数用return一次性返回结果就结束了。带yield的函数叫生成器函数,调用它不会立即执行,而是返回一个生成器对象;每次向它要一个值(next()或for迭代),函数才执行到下一个yield,吐出一个值后暂停,保留全部局部状态,下次接着往下跑。defcounter(n):print(开始)foriinrange(n):yieldi# 吐出 i,然后暂停在这里print(结束)gencounter(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(1forlineinfifERRORinline)这个版本处理 2GB 还是 200GB,内存占用都是常数级(一行的大小)。如果读的不是按行分隔,而是要按固定大小分块(比如二进制文件),自己写个生成器:defread_chunks(path,chunk_size1024*1024):按 1MB 分块惰性读取,适合大二进制文件withopen(path,rb)asf:whileTrue:chunkf.read(chunk_size)ifnotchunk:# 读到文件末尾breakyieldchunk# 吐出一块,暂停,等下次要# 用法:算一个大文件的 MD5,内存只占 1MBimporthashlibdeffile_md5(path):hhashlib.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:ifERRORinline:yieldlinedefextract_ip(lines):forlineinlines:# 假设每行以「IP 空格」开头yieldline.split( ,1)[0]# 把三级串起来:数据一条条流过,内存始终只有一行pipelineextract_ip(only_errors(read_lines(app.log)))foripinpipeline:print(ip)注意:整个pipeline在for循环真正迭代之前,一行都没读。这就是惰性求值的威力——你搭好流水线,数据才在你要的时候一条条流过。一个容易踩的坑:生成器只能消费一次生成器是一次性的,迭代完就空了,再迭代什么都没有: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是「要一个给一个」——处理大数据时,惰性才是省内存的关键。