当前位置: 首页 > 科技观察

Python进阶技巧:一行代码将内存占用减半

时间:2023-03-21 17:55:13 科技观察

我和大家分享一下我和我的团队在一个项目中遇到的一些问题。在这个项目中,我们必须存储和处理一个相当大的动态列表。在测试过程中,测试人员抱怨内存不足。这是通过添加一行代码来解决此问题的简单方法。图片的结果让我解释一下它是如何工作的。首先,我们考虑一个简单的“学习”例子,创建一个Dataltem类,它是一个人的个人信息,比如姓名、年龄、地址等。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))我们得到的答案是56bytes,它好像占用的内存很少,自己还挺满意的。因此,我们正在尝试另一个包含更多数据的示例对象:d2=DataItem("Boris",24,"Inthemiddleofnowhere")print("sys.getsizeof(d2):",sys.getsizeof(d2))答案仍然是它是56字节。这一刻,我们似乎意识到不对劲了?并非一切都像乍看起来那样。直觉不会让我们失望,一切都没有那么简单。Python是一种非常灵活的动态类型语言,它的工作存储了大量额外的数据。他们自己占据了很多。例如,sys.getsizeof("")返回33个字节,是一个最多33个字节的空行!而sys.getsizeof(1)返回24bytes,一个整数占24个字节(想请教C语言程序员,远离屏幕,不想再往下看,以免对美感失去信心).对于更复杂的元素,如字典sys.getsizeof(.())返回272个字节,这是一个空字典,我不会再进一步??,我希望理由很清楚,RAM的制造商需要出售他们的芯片。但是,让我们回到我们的DataItem类和我们最初的初学者难题。这个类占用多少内存?首先,我们以小写形式输出这个类的完整内容:defdump(obj):forattrindir(obj):print("obj.%s=%r"%(attr,getattr(obj,attr)))这个函数会揭示隐藏在“幕后”的是什么使所有Python函数(类型、继承等等)都起作用。结果令人印象深刻:这一切需要多少内存?下面有一个函数可以递归调用getsizeof函数来计算对象的实际数据大小。defget_size(obj,seen=None):#From#Recursivelyfindssizeofobjectssize=sys.getsizeof(obj)ifseenisNone:seen=set()obj_id=id(obj)ifobj_idinseen:return0#Importantmarkasseen*之前*进入递归优雅地处理#self-referential_objects()ifisinstance(obj,dict):size+=sum([get_size(v,seen)forvinobj.values()])size+=sum([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])returnssizelet让我们试试:d1=DataItem("Alex",42,"-")print("get_size(d1):",get_size(d1))d2=DataItem("Boris",24,"Inthemiddleofnowhere")print("get_size(d2):",get_size(d2))我们分别得到460bytes和484bytes的答案,貌似是真的。使用此功能,您可以执行一系列实验。例如,我想知道如果将DataItem结构放在列表中,数据将占用多少空间。get_size([d1])函数返回532bytes,这显然和上面提到的460+是一样的开销。但是get_size([d1,d2])返回863bytes,小于上面的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=address更多信息可以在文档(RTFM)中找到,其中显示“__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.使用时占用6.9MB。这个操作当然不是最好的,但它确实有最小的代码更改。(当然不是7次,但一点也不差,考虑到代码更改很少。)现在是缺点。激活__slots__会禁用所有元素的创建,包括__dict__,这意味着,例如,以下将结构转换为json的代码将不起作用:deftoJSON(self):returnjson.dumps(self.__dict__)这个问题很容易解决修复,以编程方式生成dict是否足够,循环遍历所有元素:deftoJSON(self):data=dict()forvarinself.__slots__:data[var]=getattr(self,var)returnjson.dumps(data)也不可能动态地给这个类添加新的类变量,但是在这个例子中,这是没有必要的。今天最后一次考试。有趣的是整个程序占用了多少内存。向程序添加一个无限循环,使其不会结束并查看Windows任务管理器中的内存消耗。没有__slots__:6.9Mb变成27Mb...嘿伙计们,毕竟,我们节省了内存,27Mb而不是70,对于额外的代码行来说,这是一个不错的例子注意:TraceMelc调试库使用了大量额外的内存。显然,她为每个创建的对象添加了额外的元素。如果你关闭它,总内存消耗会少很多,截图显示了两个选项:如果你想节省更多内存怎么办?这可以使用numpy库来实现,它允许您以C风格创建结构,但在我的例子中,它需要对代码进行更深入的阐述,第一种方法就足够了。奇怪的是,Habré从未详细分析过__slots__的使用,我希望这篇文章能填补这一空白。结语这篇文章看似是反Python的广告,其实不然。Python非常可靠(您必须非常努力才能“理解”Python程序),而且它是一种易于阅读和编写代码的语言。在许多情况下,这些优势胜过劣势,但如果您需要最大的性能和效率,您可以使用像numpy这样的库,它是用C++编写的,可以非常快速高效地处理数据。