我之前的文章带你揭秘了Python为内置对象分配内存时5个奇葩有趣的小秘密。本文使用sys.getsizeof()来计算内存,但是使用该方法计算时可能会出现意想不到的问题。文档中对该方法的介绍有两层意思:该方法用于获取对象的字节大小(bytes)。它只计算直接占用的内存,不计算对象中引用的对象的内存。也就是说,getsizeof()计算的不是实际对象的字节大小,而是“占位符对象”的大小。如果要计算所有属性的大小和属性的属性,getsizeof()只会停留在第一层,对于有引用的对象计算时不准确。比如列表[1,2],getsizeof()不统计列表中两个元素的实际大小,只统计对它们的引用。举个形象的例子,我们把列表想象成一个盒子,里面存放的对象是球。现在箱子里有两张纸条,上面写着球1和球2的地址(球不在箱子里),getsizeof()只是称了整个箱子(包括纸条),并没有找到两张根据纸条上的地址将球一起称重。1.计算的是什么?我们先看一下list对象:如图,分别计算a和b列表的结果是36和48,然后当它们作为c列表的子元素时,计算结果为列表只有36个。(PS:我用的是32位的解释器)如果不用引用的方式,直接写子列表,比如"d=[[1,2],[1,2,3,4,5]]”,所以d表的计算结果还是36,因为子表是独立的对象,它们的id都保存在d表中。也就是说:getsizeof()方法计算列表大小时,结果与元素个数有关,与元素本身的大小无关。我们再来看字典的例子:很明显三个字典实际占用的总内存不可能相等,但是getsizeof()方法给出的结果是一样的,也就是说它只关心key的个数,not实际的键值对是什么,情况和列表类似。2.“浅计算”等问题有一个概念叫“浅拷贝”,意思是copy()方法只是拷贝被引用对象的内存地址,而不是实际的被引用对象。类比这个概念,我们可以把getsizeof()看作是一个“浅层计算”。“浅层计算”不关心真实的对象,所以它的计算结果只是一种假象。这是一个值得注意的问题,但仅仅注意到这一点还不够。我们还可以发散思考以下问题:“浅计算”方式的底层实现是什么?为什么getsizeof()采取“浅计算”的方式?关于第一个问题,getsizeof(x)方法实际上调用了x对象的__sizeof__()魔术方法,这是CPython解释器针对内置对象实现的。找到这篇分析CPython源码的文章《Python中对象的内存使用(一)》,终于定位到核心代码:/*longobject.c*/staticPy_ssize_tint___sizeof___impl(PyObject*self){res=offsetof(PyLongObject,ob_digit)+Py_ABS(Py_SIZE(self))*sizeof(digit);returnres;}这段代码我看不懂,但我能知道的是,在计算Python对象的大小时,它只是遵循对象属性的结构,没有进一步的“深度计算”。对于CPython的这个实现,我们可以注意到两个层面的区别:字节增加:int类型在C语言中只占用4个字节,但是在Python中,int实际上被封装成一个Object,所以在计算它的大小时,对象结构将被包括在内。在32位解释器上,getsizeof(1)的结果是14个字节,比数字本身的4个字节有所增加。字节缩减:对于比较复杂的对象,比如列表、字典,这种计算机制会导致比实际内存占用更小的内存,因为它没有累加内部元素的占用量。由此,我有一个不成熟的猜测:基于“一切皆对象”的设计原则,int等基本的C数据类型在Python中被套上了一层“外壳”,因此需要一种方法来计算它们的Size,即是getsizeof()。官方文档说“Allbuilt-inobjectswillreturncorrectresults”[1],应该是指数字、字符串、布尔值等简单对象。但是,它不包括具有内部引用关系的列表、元组和字典等类型。为什么不推广到所有内置类型?我还没有找到对此的解释,如果有人知道,请告诉我。3.“深度计算”等问题对应“浅层计算”,我们可以定义一种“深度计算”。对于前面两个例子,“深度计算”应该遍历每一个内部元素和可能的子元素,累加计算它们的字节数,最后计算出总的内存大小。那么,我们应该关注的问题是:“深度计算”是否有方法/实施方案?实施“深度计算”需要注意什么?Stackoverflow网站上有一个老问题“HowdoIdeterminethesizeofanobjectinPython?”[2],其实是在问如何实现“深度计算”。有两个项目由不同的开发人员贡献:pympler和pysize:第一个项目已经在Pypi上发布,可以使用“pipinstallpympler”安装;第二个项目未完成,作者并没有在Pypi上发布(注意:Pypi上已经有一个pysize库,用于格式转换,不要混淆),但其源代码可以在Github上获取.对于前面两个例子,我们可以分别测试这两项:单看值,pympler似乎比getsizeof()要合理很多。再看pysize,直接看测试结果(获取其源码的过程略):6411819020630028130281,可以看出比pympler计算的结果略小。从两个项目的完整性、使用率和社区贡献者规模来看,pympler的结果似乎更可信。那么,他们是如何实现的呢?这种微小的差异是如何产生的?我们可以从他们的实施中学到什么?pysize项目很简单,只有一个核心方法:defget_size(obj,seen=None):"""递归查找对象的大小,以字节为单位"""size=sys.getsizeof(obj)ifseenisNone:seen=set()obj_id=id(obj)ifobj_idinseen:return0#重要标记asseen*before*进入递归以优雅地处理#self-referentialobjectsseen.add(obj_id)ifhasattr(obj,'__dict__'):forclsinobj.__class__.__mro__:if'__dict__'incls.__dict__:d=cls.__dict__['__dict__']ifinspect.isgetsetdescriptor(d)orinspect.ismemberdescriptor(d):size+=get_size(obj.__dict__,seen)breakifisinstance(obj,dict):size+=sum((get_size(v,seen)forvinobj.values()))size+=sum((get_size(k,seen)fork在obj.keys()))elifhasattr(obj,'__iter__')而不是isinstance(obj,(str,bytes,bytearray)):size+=sum((get_size(i,seen)foriinobj))ifhasattr(obj,'__slots__'):#可以有__slots__和__dict__size+=sum(get_size(getattr(obj,s),seen)forsinobj.__slots__ifhasattr(obj,s))returnsize去掉判断__dict__和__slots__属性的部分(对于类对象),主要是针对字典类型和可迭代对象(字符串、字节、字节数组除外)进行递归计算,逻辑并不复杂。以列表[1,2]为例,它首先使用sys.getsizeof()计算出36个字节,然后计算两个内部元素得到14*2=28个字节。最后加起来得到64个字节。相比之下,pympler考虑的内容要多得多,入口在这里:defasizeof(self,*objs,**opts):'''返回给定对象的组合大小(修改选项,见方法**set**).'''ifopts:self.set(**opts)self.exclude_refs(*objs)#skiprefstoobjsreturnsum(self._sizer(o,0,0,None)foroinobjs)它可以接受多个参数,然后使用sum()方法进行组合。所以核心的计算方法其实就是_sizer()。但代号很复杂,绕过像一座迷宫:def_sizer(self,obj,pid,deep,sized):#MCCABE19'''递归地调整对象的大小。'''s,f,i=0,0,id(obj)ifinotinself._seen:self._seen[i]=1elifdeeporself._seen[i]:#skipobjifseenbefore#或者如果给定对象的引用self._seen.again(i)如果大小:s=sized(s,f,name=self._nameof(obj))self.exclude_objs(s)returns#zeroelse:#deep==seen[i]==0self._seen.again(i)尝试:k,rs=_objkey(obj),[]ifkinself._excl_d:self._excl_d[k]+=1else:v=_typedefs.get(k,None)ifnotv:#newtypedef_typedefs[k]=v=_typedef(obj,derive=self._derive_,frames=self._frames_,infer=self._infer_)if(v.bothorself._code_)andv.kindisnotself._ign_d:#猫注:这里计算flatsizes=f=v.flat(obj,self._mask)#flatsizeifself._profile:#profilebasedon*flat*sizeself._prof(k).update(obj,s)#递归,但不适用于嵌套模块ifv.refsanddeep
