作者:猫下豌豆花来源:Python猫我之前的文章揭秘了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解释器针对内置对象实现的。找到这篇文章《Python中对象的内存使用(一)》,分析了CPython源码,终于定位到核心代码:这段代码我看不懂,但我知道的是,在计算一个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,直接看测试结果(获取其源码的过程略):可以看出比pympler计算的结果略小。从两个项目的完整性、使用率和社区贡献者规模来看,pympler的结果似乎更可信。那么,他们是如何实现的呢?这种微小的差异是如何产生的?我们可以从他们的实施中学到什么?pysize项目很简单,核心方法只有一个:去掉判断__dict__和__slots__属性的部分(针对类对象),主要对字典类型和可迭代对象(除了strings、bytes、bytearray)进行递归计算,逻辑并不复杂。以列表[1,2]为例,它首先使用sys.getsizeof()计算出36字节,然后计算内部的两个元素得到14*2=28字节,最后相加得到64字节。相比之下,pympler考虑的内容就多了很多,入口在这里:它可以接受多个参数,然后使用sum()方法将它们组合起来。所以核心的计算方法其实就是_sizer()。但是代码非常复杂,看起来像一个迷宫:它的核心逻辑是将每个对象的大小分为平面大小和项目大小两部分。计算flatsize的逻辑是:这里出现的掩码是为了字节对齐,默认值为7。计算公式表明是按8字节对齐的。对于[1,2]列表,计算结果为(36+7)&~7=40字节。同样,对于单个项,比如列表中的数字1,sys.getsizeof(1)等于14,pympler会把它算作对齐值16,所以和就是40+16+16=72字节。这就解释了为什么pympler计算的结果比pysize大。字节对齐一般由特定的编译器实现,不同的编译器有不同的策略。理论上,Python不应该关心这些底层细节。内置的getsizeof()方法不考虑字节对齐。在不考虑其他边缘情况的情况下,可以认为pympler是基于getsizeof()的,它不仅考虑了遍历fetch引用对象的大小,还考虑了实际存储中的字节对齐问题,所以会显得更接近现实。4.总结getsizeof()方法的问题很明显,我为它创造了一个“浅计算”的概念。这个概念是借用copy()方法的“浅拷贝”,对应deepcopy()的“深拷贝”,我们也可以推导出一个“深计算”。上面展示了两个试图实现“深度计算”的项目(pysize+pympler)。两者都是在浅层计算的基础上,深入解决参照物的尺寸问题。pympler项目的完整性很高,代码中有很多细节设计,比如字节对齐。当然,Python官方团队也意识到了getsizeof()方法的局限性,他们甚至在文档中添加了一个链接[3],指向实现深度计算的示例代码。该代码甚至比pysize更简单(不考虑类对象)。以后Python会有深度计算的方法吗,假设叫getdeepsizeof()?这是未知的。本文旨在加深对getsizeof()方法的理解,区分浅层计算和深度计算,分析两个深度计算项目的实现思路,指出几个值得注意的问题。看完本文,希望你也能有所收获。如果您有任何想法,请与我们分享。
