没有内存问题,是一行Python代码解决不了的问题也遇到了这个问题,我们的项目需要存储和处理一个比较大的动态列表,测试人员经常向我抱怨内存不足.但最终,我们通过添加简单的一行代码解决了问题。结果如图所示:我将在下面解释它是如何工作的。举一个简单的“学习”示例——创建一个DataItem类,您可以在其中定义一些个人信息属性,例如姓名、年龄和地址。classDataItem(object):def__init__(self,name,age,address):self.name=nameself.age=ageself.address=address小测试——这样一个对象占用多少内存?首先让我们尝试以下测试方案:d1=DataItem("Alex",42,"-")print("sys.getsizeof(d1):",sys.getsizeof(d1))答案是56个字节。看起来比较小,效果还算满意。但是让我们检查另一个包含更多数据的对象:d2=DataItem("Boris",24,"Inthemiddleofnowhere")print("sys.getsizeof(d2):",sys.getsizeof(d2))答案仍然是56。这让我们看到这个结果并不完全正确。我们的直觉是对的,问题并没有那么简单。Python是一种非常灵活的动态类型语言,它在工作时会存储大量额外数据。这些额外的数据本身会占用大量内存。比如sys.getsizeof("")返回33,没错,每个空行最多33个字节!而sys.getsizeof(1)会为这个数字返回24-24个字节(我建议C程序员们现在点击看完,以免对Python的美感失去信心)。对于更复杂的元素,例如字典,sys.getsizeof(dict())返回272个字节,这只是一个空字典。例子就这么多,但事实很清楚,RAM制造商也需要销售他们的芯片。现在,让我们回到我们的DataItem类和“小测试”问题。这个类占用多少内存?首先,我们将在较低级别输出类的全部内容:defdump(obj):forattrindir(obj):print("obj.%s=%r"%(attr,getattr(obj,attr)))这个function将揭示隐藏在“斗篷”下的内容,以便所有Python函数(类型、继承和其他包)都可以工作。结果令人印象深刻:总共需要多少内存?GitHub上有一个计算实际大小的函数,通过对所有对象递归调用getsizeof实现。defget_size(obj,seen=None):#Fromhttps://goshippo.com/blog/measure-real-size-any-python-object/#Recursivelyfindssizeofobjectssize=sys.getsizeof(obj)ifseenisNone:seen=set()obj_id=id(obj)ifobj_idinseen:return0#Importantmarkasseen*before*enteringrecursiontogracefullyhandle#self-referentialobjectsseen.add(obj_id)ifisinstance(obj,dict):size+=sum([get_size(v,seen)forvinobj.values()])大小+=总和([get_size(k,seen)forkinobj.keys()])elifhasattr(obj,'__dict__'):size+=get_size(obj.__dict__,seen)elifhasattr(obj,'__iter__')andnotisinstance(obj,(str,bytes,bytearray)):size+=sum([get_size(i,seen)foriinobj])returnsize让我们试试:d1=DataItem("Alex",42,"-")print("get_size(d1):",get_size(d1))d2=DataItem("Boris",24,"Inthemiddleofnowhere")print("get_size(d2):",get_size(d2))我们分别得到460和484字节,这似乎更接近事实。使用这个函数,我们可以进行一系列的实验。例如,我想知道如果将DataItem放在列表中,数据将占用多少空间。get_size([d1])函数返回532个字节,显然,这些是“原始”460+一些开销。但是get_size([d1,d2])返回863字节-小于460+484。get_size([d1,d2,d1])的结果比较有意思,它产生了871个字节,只是稍微多了一点,这说明Python很聪明,不会为同一个对象再次分配内存。现在我们来看问题的第二部分。是否可以减少内存消耗?答案是肯定的。Python是一个解释器,我们可以随时扩展我们的类,例如添加一个新的字段:d1=DataItem("Alex",42,"-")print("get_size(d1):",get_size(d1))d1.weight=66print("get_size(d1):",get_size(d1))这是一个很棒的特性,但是如果我们不需要这个特性,我们可以强制解释器使用__slots__指令来指定类属性列表:classDataItem(object):__slots__=['name','age','address']def__init__(self,name,age,address):self.name=nameself.age=ageself.address=addressFor更多信息,请参阅文档中的“__dict__和__weakref__的一部分。使用__dict__节省的空间可能很重要”。我们试了之后发现get_size(d1)返回的是64字节,比460字节少了大约7倍。作为奖励,对象创建速度提高了大约20%(参见文章的第一个屏幕截图)。真正使用如此大的内存增益没有其他开销成本。只需添加元素即可创建100,000个数组并查看内存消耗:data=[]forpinrange(100000):data.append(DataItem("Alex",42,"middleofnowhere"))snapshot=tracemalloc.take_snapshot()top_stats=snapshot.statistics('lineno')total=sum(stat.sizeforstatintop_stats)print("Totalallocatedsize:%.1fMB"%(total/(1024*1024)))在没有__slots__的情况下产生16.8MB,而在使用__slots__时产生6.9MB。当然不是7倍,但考虑到小的代码更改,它仍然表现出色。现在讨论这种方法的缺点。激活__slots__会禁用所有其他元素的创建,包括__dict__,这意味着,例如,以下将结构转换为json的代码将不起作用:deftoJSON(self):returnjson.dumps(self.__dict__)但这也是很容易做到,以编程方式生成你的字典,循环遍历所有元素:deftoJSON(self):data=dict()forvarinself.__slots__:data[var]=getattr(self,var)returnjson.dumps(data)它是也不可能动态地向类中添加新变量,但在我们的项目中,这是没有必要的。这是最后一个小测试。让我们看看整个程序需要多少内存。在程序末尾添加一个无限循环以使其保持运行并在Windows任务管理器中观察内存消耗。69Mb在没有__slots__的情况下变成了27Mb...好吧,毕竟我们节省了内存。对于仅添加一行代码的结果来说,这已经足够好了。注意:tracemalloc调试库使用大量额外内存。显然它为每个创建的对象添加了额外的元素。如果关闭它,总内存消耗会少很多,屏幕截图显示2个选项:如何节省更多内存?你可以使用numpy库,它允许你创建C风格的结构,但在这个项目中,它需要更深入地改进代码,所以对我来说第一种方法就足够了。奇怪的是,哈布雷从未对__slots__的使用进行过详细分析,我希望这篇文章能够填补这一空白。结束语这篇文章可能看起来像一个反Python广告,但它根本不是。Python非常扎实(你必须非常努力地使用Python来“删除”一个程序),而且它是一种易于阅读和编写的语言。在很多情况下,这些优点远远超过缺点,但是如果你需要最大的性能和效率,你可以使用numpy库来编写像C++这样的代码,它可以非常快速和高效地处理数据。最后祝大家编程愉快!相关报道:https://medium.com/@alexmaisiura/python-how-to-reduce-memory-consumption-by-half-by-adding-just-one-line-of-code-56be6443d524【本文为栏目组织大数据文摘原译微信公众号《大数据文摘(id:BigDataDigest)》】点此查看本作者更多好文
