前几天有个朋友找我吐槽,他写了一段代码,用readlines()读一个几百兆的日志文件,结果程序直接卡住,内存飙到好几个G。他问我:“难道Python读大文件就没办法吗?”我笑了,告诉他这不是Python的问题,是他的方法不对。
为了让你更直观感受一下,我们来看两种代码。
第一种写法:
with open('big_file.txt', 'r') as f: lines = f.readlines()
for line in lines:
process(line)
第二种写法:
with open('big_file.txt', 'r') as f: for line in f:
process(line)
看起来差不多,对吧?但第一种会把整个文件加载到内存,第二种是一次读一行。你猜猜第二种背后用了什么技巧?
没错,就是迭代器模式。文件对象在Python里本身就是个迭代器,你写for line in f的时候,它不会一次性把文件全吞进去,而是每次只取一行数据到内存里。处理完这行,内存就释放,再取下一行。这就跟你吃一串葡萄一样,一颗颗摘着吃,不用把整串葡萄全塞嘴里。
很多人写代码时容易犯一个错误,就是图省事直接调read()或者readlines()。这两个方法方便是方便,但你得知道它们背后的代价。尤其是当文件有几十G的时候,你电脑怎么扛得住?
我自己以前也吃过这个亏。那时候在做一个数据统计分析的任务,每天要处理好几个G的CSV文件。一开始我也是傻乎乎的用read_csv不加参数,结果程序跑着跑着就报内存错误。后来发现pandas的read_csv其实有个chunksize参数,就是基于迭代器思想设计的。你把一个大文件分成小块,一块块加载,每块处理完就扔,内存一直很平稳。
迭代器模式的好处不止是省内存。它还能让你的代码更易读,更不容易出错。比如有时候你要从一个大文件里找某条记录,用迭代器就能在找到后立即停止,不用把整个文件读完。这就跟翻书一样,你想找某个字,翻到了就停手,不用把一本书全背下来。
在Python里,除了文件,还有很多内置对象支持迭代。比如range函数,你写range(108)不会一次性创建一亿个数字,而是每次迭代时才生成下一个数字。还有字典、集合,都能用for循环直接遍历。这些都是迭代器模式的应用。
如果你自己写类,也可以让它支持迭代。只需要实现__iter__和__next__这两个方法,或者用yield生成器函数。生成器是迭代器的一个简化版本,你可以在函数里写个yield,每次调用都返回一个值,暂停在那里,等下次调用再继续。这个特性特别适合处理流式数据,比如读取网络数据,或者实时传感器信号。
我见过很多人,明明做的是数据处理,却还按处理小文件的方式去堆内存。结果每次跑任务都心惊胆战,生怕内存爆了。你用迭代器模式,就像给自己上了个保险,怎么跑都不慌。
最后送你一句实在的话:能用迭代器的地方,就别把数据全装进内存里。这不是偷懒,是聪明。下次再遇到大文件卡死,先想想是不是该用迭代器救了。你试试就知道了,手感完全不一样。