看着那个红红的报错信息,心里又急又烦。尤其是当数据量特别大的时候,比如几百万行CSV文件,或者几十G的日志,电脑风扇呼呼转,最后弹出一个MemoryError。 我刚开始学Python的时候,觉得列表特别方便。什么东西都往列表里塞,readlines读文件,一口气全塞进内存。结果有一次处理一个5G的文本,电脑直接卡死,强制关机都没用。那次之后我才知道,列表虽然好用,但在大数据面前就是内存杀手。
后来我学会了生成器。说真的,这东西就像变魔术。同样处理一个大文件,用生成器写法,内存占用从几百兆降到几十KB。不信你可以试试,读一个1G的文件,分别用read和生成器方式,打开任务管理器看看差距。
生成器是什么?其实很简单。它就像一个“随取随用”的流水线。你只需要告诉它“我下一个要什么”,它就去生产一个,生产完就给你,不存任何旧东西。你不需要一次性把所有数据都搬进家里,用的时候才去拿。
拿最常见的文件处理来说。以前我这样写:
f = open('bigfile.txt')
lines = f.readlines()
for line in lines:
do_something(line)
这就把整个文件都读到内存里了。如果是500M的文件,这500M就全占住了。 用生成器怎么写?
with open('bigfile.txt') as f:
for line in f:
do_something(line)
你发现了没,去掉readlines,直接在文件对象上循环,它就是生成器。每次只读一行,内存里永远只有这一行。处理完一行,下一行进来,上一行就被垃圾回收了。 再举个例子,比如你要生成一百万个数字。用列表:
nums = [i for i in range(1000000)]
立刻占用几十兆内存。换成生成器:
nums = (i for i in range(1000000))
内存占用几乎为零。因为它不一次性生成,只有你for循环去拿它的时候,它才一个一个往外吐。 还有更高级的用法,比如用yield自己写生成器函数。我以前写过一个日志解析器,每天处理上百G的访问日志。用yield写了一个行读取器,每读一行就yield出去,主程序处理完这一行,再回来继续读下一行。整个程序跑下来,内存占用就稳定在几百兆,从来没有报过错。
你可能会问,那是不是所有数据都要用生成器?也不是。如果你数据量不大,或者你需要多次遍历同批数据,列表反而更方便。生成器只能遍历一次,用完就没了,这点要注意。
我见过很多新手一上来就喜欢写列表推导式,其实很多时候用生成器推导式更好。比如要过滤一些数据,列表推导式写出来很漂亮,但问题是有时候你根本不需要全部数据,只是想看看前几个结果。用生成器推导式配合next函数,取前几个值的时候,内存开销微乎其微。做数据分析的朋友更容易遇到这个问题。pandas读CSV文件,很多人上来就pd.read_csv,大一点的文件直接炸。其实pandas本身就支持分块读取,参数里加个chunksize=10000,返回的就不是DataFrame,而是一个生成器对象。然后你就可以一块一块地处理,处理完一块释放一块,再也不怕数据太大。
生成器这种工具,一旦用习惯了你就会上瘾。写代码的时候多想想,这一步是不是真的需要把所有数据都攒齐了再干?能不能来一个处理一个?能来一个处理一个,就别攒一堆再处理。
最后提醒一下,用生成器要注意的是,别把生成器对象存到列表里。我见过有人写:
results = list(some_generator())
这等于又把生产出来的东西全堆到内存里了,那就白费功夫了。直接循环就好。 说实话,做数据处理这块,能把内存控制住,很多问题就都不是问题了。生成器这个技巧不复杂,但确实是最实用的一个。你现在就可以打开电脑,找一个平时处理起来会卡顿的大文件,试试用生成器来读,看看内存占用是不是明显降下来了。