没想到有一天写 python
的时候也会想着如何去节省内存。平时写 python
的时候根本不会关注这些,变量什么的直接创建和使用就完了,也不用考虑内存的释放,反正有垃圾回收机制。只不过这次数据量过大,debug
的时候发现内存一直在申请,导致系统彻底的卡死。
可能也是从事算法的优化工作养成了职业病,每次写代码的时候都会想,这些代码消耗的时间怎么样,占用的空间怎么样,数据结构是否可以继续优化,这些逻辑有没有更优雅的写法。
注:本文程序中使用 psutil
库来监测进程使用的内存大小,需要 pip install psutil
一下。
背景
需要解析一个很大的日志文件,日志文件中含有一些无用的信息,像下面这样:
1 | 有用信息1 |
解析文件的时候,需要从文件中解析并提取出有用的信息,存入一个对象中,完成后续的处理。
但是呢,对于某些特殊的任务和需求,发现文件只解析一次是不行的,也就是需要对文件进行二次解析。
所以为了避免重复的解析文件,在第一次文件解析完毕后,直接把有用的核心信息序列化出去,这样二次解析的话就不用重新读取源文件在解析,直接读取序列化后的核心数据就好了。
序列化导出
最开始的方案是使用一个 list
持续追加解析得到的核心数据,文件解析完毕后把这个很大的 list
序列化出去。监测到进程占用的内存大小为:700MB。
1 | import random |
而如果使用序列化追加的方式,仅用 15MB,耗时增加 2s,毕竟每次序列化的时候都需要打开文件并在末尾追加内容:
1 | with open("data.pkl", "ab") as f: |
这里可以设置一个 buffer
进行优化,buffer
达到一定大小后在统一序列化出去。
1 | class SeriesModel: |
序列化读入
在二次解析的时候,需要把序列化的数据 load
进来。如果加载序列化的文件并且直接处理数据,同样需要使用 700MB 的内存。这种一次性创建所有元素的行为是没有必要的。
1 | with open("data.pkl", "rb") as f: |
可以使用惰性计算来解决这一问题,只有在真正需要这个变量的时候才去创建,而不是一开始就创建所有的变量。考虑到生成器表达式的局限性,我们直接使用 yield
关键字创建一个生成器函数。
yield
语句类似 return
会返回一个值,但它会记住这个返回的位置,下次迭代的时候就从这个位置继续执行,返回下一个元素。这样就消耗内存 15MB。
1 | def read(file): |
引申
任何一个生成器都会定义一个名为 __next__
的方法,这个方法要在最后一个元素之后需抛出 StopIteration
异常。next()
函数的本质就是调用对象的 __next__()
。这个方法要么返回迭代的下一项,要么引起结束迭代的异常 StopIteration
,下面的示例揭示了生成器的本质。
1 | class FibGenerator(): |
示例中如果没有定义 __iter__()
方法则只能使用 next()
函数进行迭代,当它定义后,就可以使用 for
和 in
语句访问了,同时定义了这两种方法的对象称为迭代器。生成器表达式和生成器函数产生生成器时,会自动生成名为 __iter__
和 __next__
的方法,所以生成器也是一种迭代器。
参考链接
https://pythonhowto.readthedocs.io/zh-cn/latest/iterator.html