独家|测量、建议、快速上手!你所使用的Python对象占用了多少内存?(附代码)

简介: 本文将介绍多种Python对象分别所占用的内存,并解释所选择的测量方法和函数,为节省内存提供建议。

作者:Gigi Sayfan

文章来源:微信公众号 数据派THU

翻译:吴振东

校对:吴金笛


本文将介绍多种Python对象分别所占用的内存,并解释所选择的测量方法和函数,为节省内存提供建议。

Python是一种很棒的编程语言。不过它的运行速度很慢,这是由于它具有极大的灵活性和动态特征所造成的。对于许多应用和领域来说,考虑到它们的要求和各种优化技术,这并不能算是一个问题。众所周知,Python对象图(列表、元组和基元类型的嵌套字典)占用了大量内存。这可能是一个更为严格的限制因素,因为这对缓存、虚拟内存、与其他程序的多租户产生了影响,而且通常会更快地耗尽一种稀缺且昂贵的资源——可用内存。

事实证明,想要弄清楚实际消耗了多少内存并非易事。在本文中,我将向你介绍Python对象内存管理的复杂性,并展示如何准确地去测量所消耗的内存。

在本文中,我只关注CPython——Python编程语言的主要实现。这里的实验结论并不适用于其他Python的实现,例如IronPython,Jython和PyPy。

另外,我是在Python 2.7上运行所得到的这些数据。如果是在Python 3中,这些结果可能会略有不同(特别是对于Unicode的字符串),但是理念是基本相同的。

关于Python内存使用的实践探索

首先,让我们初步探索一下,来了解Python对象的实际内存使用的具体情况。

内嵌函数sys.getsizeof()

标准库的sys模块提供了getsizeof()函数。该函数接收一个对象(和可选的默认值),调用sizeof()方法并返回结果,从而可以让你所使用的对象具备可检查性。

getsizeof()

https://mp.weixin.qq.com/cgi-bin/appmsg?t=media/appmsg_edit_v2&action=edit&isNew=1&type=10&token=1853049065&lang=zh_CN#sys.getsizeof

测量Python对象的内存

首先从数值类型开始:

python import sys

sys.getsizeof(5) 24  

有意思,一个整数(integer)占用了24字节。

python sys.getsizeof(5.3) 24

嗯……一个浮点数(float)同样占用24字节。

python from decimal import Decimal sys.getsizeof(Decimal(5.3)) 80

哇哦,80字节!如此一来你可能要想一想是该用float还是Decimals来表示大量的实数了。

让我们看一下字符串(strings)和collections:

python sys.getsizeof(‘’) 37 sys.getsizeof(‘1’) 38 sys.getsizeof(‘1234’) 41

sys.getsizeof(u’’) 50 sys.getsizeof(u’1’) 52 sys.getsizeof(u’1234’) 58

好吧。一个空字符串占用37字节,每增加一个字符就增加1个字节。这提出了一个关于对保留多个短字符串的权衡问题,你是愿意为每个短字符串支付37字节的开销,还是愿意为一个长字符串一次性地支付开销。

Unicode字符串的行为类似,但它的开销是50字节,每增加一个字符就会增加2字节的开销。如果你使用返回Unicode字符串的库,而你的文本原本可以用简单的字符串来表示的话,那么你就需要考虑下这一点。

顺便说一下,在Python 3中,字符串都是Unicode,开销是49字节(它们在某处节省了1字节)。Bytes对象的开销是33字节。如果你的程序在内存中需要处理大量的短字符串,而你又很关心程序的性能的话,那么建议你考虑使用Python 3。

python sys.getsizeof([]) 72 sys.getsizeof([1]) 88 sys.getsizeof([1, 2, 3, 4]) 104 sys.getsizeof(['a long longlong string'])

这是怎么回事?一个空的list占用72字节,但每增加一个int只加大了8字节,其中一个int占用24字节。一个包含长字符串的list只占用80字节。

答案其实很简单。list并不包含int对象本身。它只包含一个占8字节(在CPython 64位版本中)指向实际int对象的指针。这意味着getsizeof()函数不返回list的实际内存及其包含的所有对象,而只返回list的内存和指向其对象的指针。

在下一节中,我将介绍可以解决此问题的deep_getsizeof()函数。

python sys.getsizeof(()) 56 sys.getsizeof((1,)) 64 sys.getsizeof((1, 2, 3, 4)) 88 sys.getsizeof(('a long longlong string',)) 64

对于元组(tuples)来说情况类似。空元组的开销是56字节,空list是72字节。如果你的数据结构包括许多小的不可变的序列,那么每个序列之间所差的这16字节是一个非常容易实现的目标。

python sys.getsizeof(set()) 232 sys.getsizeof(set([1)) 232 sys.getsizeof(set([1, 2, 3, 4])) 232

sys.getsizeof({}) 280 sys.getsizeof(dict(a=1)) 280 sys.getsizeof(dict(a=1, b=2, c=3)) 280

当你添加一个项时,集合(Set)和字典(dictionary)在表面上根本不会有所增长,但请注意它们所带来的巨大开销。

原因是Python对象具有巨大的固定开销。如果你的数据结构由大量的集合对象组成,比如说字符串、列表和字典,每个集合都包含少量的项,你同样要为之付出沉重的代价。

deep_getsizeof()函数

现在你可能被我上面所提到的吓出一身冷汗,这同时也证明了sys.getsizeof()只能告诉你原始对象需要多少内存,那么让我们来看一种更合适的解决方案。

deep_getsizeof()是向下层递归的函数,并且可以计算Python对象图的的内存实际使用量。

python from collections import Mapping, Container from sys import getsizeof

def deep_getsizeof(o, ids): “"”Find the memory footprint of a Python object

这是一个递归函数,它向下读取一个Python对象图,比如说一个包含列表套用列表的嵌套字典的字典和元组以及集合。

sys.getsizeof函数仅执行较浅的深度。不管它的容器内的每个对象的实际大小,它都将其设为指针。

:param o: the object

:param ids:

:return:

"""

d = deep_getsizeof

if id(o) in ids:

    return 0



r = getsizeof(o)

ids.add(id(o))



if isinstance(o, str) or isinstance(0, unicode):

    return r



if isinstance(o, Mapping):

    return r + sum(d(k, ids) + d(v, ids) for k, v in o.iteritems())



if isinstance(o, Container):

    return r + sum(d(x, ids) for x in o)



return r   

对于这个函数来说有几个有趣的方面。它会考虑多次引用的对象,并通过追踪对象ID来对它们进行一次计数。这一实现的另一个有趣的特性是它充分利用了collections模块的抽象基类。这使得这个函数可以非常简洁地处理任何实现Mapping和Container基类的集合,而不是直接去处理无数集合类型,例如:字符串、Unicode、字节、列表、元组、字典、frozendict, OrderedDict, 集合、 frozenset等等。

让我们看下它是如何执行的:


python x = '1234567' deep_getsizeof(x, set()) 44

一个长度为7的字符串占用了44字节(原开销37字节+7个字符占用7字节)。

python deep_getsizeof([], set()) 72

空列表占用72字节(只有原开销)。

python deep_getsizeof([x], set()) 124

一个包含字符串x的列表占用124字节(72+8+44)。

python deep_getsizeof([x, x, x, x, x], set()) 156

一个包含5个x字符串的列表占用156字节(72+5*8+44)。

最后一个例子显示了deep_getsizeof()只计算一次同一对象(x字符串)的引用,但会把每一个引用的指针计算在内。

处理方式or骗招

事实证明,CPython中有一些骗招,所以你从deep_getsizeof()中所得到的数字并不能完全代表Python程序中的内存使用。

引用计数

Python使用引用计数语义来管理内存。一旦对象不再被使用,就会释放其内存。但只要存在引用,该对象就不会被释放。那些循环引用之类的东西会让你感到很难受。

小对象

CPython可以管理8字节边界上的特殊池里的小对象(小于256字节)。有1-8字节的池,9-16字节的池,一直到249-256字节的池。当一个10字节大小的对象被分配时,它会从16字节池中分配出大小为9-16字节的对象。因此,即便他只包含10字节的数据,但它还是会花费16字节的内存。如果1,000,000个10字节大小的对象被分配时,实际使用的内存是16,000,000字节,而不是10,000,000个字节。这其中多出的60%的开销显然是微不足道的。

整数

CPython保留了【-5,256】范围内所有整数的全局列表。这种优化策略是很有意义的,因为小整数随时随地都可能会出现。假设每个整数占用24个字节,那么这就会为典型的程序节省大量内存。

这意味着CPython为所有这些整数都预先分配了266*24=6384个字节,即便它们中的大部分你用不到。你可以使用id()函数来验证它,这个函数提供指向实际函数的指针。如果对【-5,256】范围内的任意x多次调用id(x),那么每次都会得到相同的结果(对于相同的整数)。但如果你拿超出这个范围的整数做尝试,那么每次得到的结果都不相同(每次都会动态创造一个新的对象)。

这有几个在这个范围内的例子:

 python id(-3) 140251817361752

id(-3) 140251817361752

id(-3) 140251817361752

id(201) 140251817366736

id(201) 140251817366736

id(201) 140251817366736 

这有几个超过这个范围的例子:

 python id(301) 140251846945800

id(301) 140251846945776

id(-6) 140251846946960

id(-6) 140251846946936  

Python内存vs系统内存

CPython具有一种所属性。在很多情况下,当程序中的内存对象不再被引用时,他们不会再返回系统中(例如小对象)。如果你分配和释放许多对象(属于同一个8字节池的),这会对你的程序很有好处,因为不需要去打扰系统,否则代价会是非常昂贵的。不过如果你的程序通常在使用X字节并在偶然情况下使用它100次(例如仅在启动时解析和处理大配置文件),那么效果就不是特别好了。

现在,100X的内存有可能被毫无用处的困在你的程序里,永远不会被再次利用,而且也拒绝被系统分配给其他程序。更具讽刺意义的是,如果你使用处理模块来运行程序的多个实例,那么就会严重限制你在给定计算机上可以运行的实例数。

内存剖析

想要衡量和测量程序的实际内存使用情况,可以使用memory_profiler模块。我尝试了一下,不确定所得出的结果是否可信。它使用起来非常简单。你装饰一个函数(可能是@profiler装饰器的主函数0函数),当程序退出时,内存分析器会打印出一份标准输出的简洁报告,显示每行的总内存和内存变化。我是在分析器下运行的这个示例。

memory_profiler

https://pypi.python.org/pypi/memory_profiler



python from memory_profiler import profile

@profile def main(): a = [] b = [] c = [] for i in range(100000): a.append(5) for i in range(100000): b.append(300) for i in range(100000): c.append(‘123456789012345678901234567890’) del a del b del c

print 'Done!' if __name__ == '__main__':

main()  
 

 
Here is the output:

 
 
Line #    Mem usage    Increment   Line Contents

================================================

     3     22.9 MiB      0.0 MiB   @profile

     4                             def main():

     5     22.9 MiB      0.0 MiB       a = []

     6     22.9 MiB      0.0 MiB       b = []

     7     22.9 MiB      0.0 MiB       c = []

     8     27.1 MiB      4.2 MiB       for i in range(100000):

     9     27.1 MiB      0.0 MiB           a.append(5)

    10     27.5 MiB      0.4 MiB       for i in range(100000):

    11     27.5 MiB      0.0 MiB           b.append(300)

    12     28.3 MiB      0.8 MiB       for i in range(100000):

    13     28.3 MiB      0.0 MiB           c.append('123456789012345678901234567890')

    14     27.7 MiB     -0.6 MiB       del a

    15     27.9 MiB      0.2 MiB       del b

    16     27.3 MiB     -0.6 MiB       del c

    17

    18     27.3 MiB      0.0 MiB       print 'Done!'

如你所见,这里的内存开销是22.9MB。在【-5,256】范围内外添加整数和添加字符串时内存不增加的原因是在所有情况下都使用单个对象。目前尚不清楚为什么第8行的第一个range(1000)循环增加了4.2MB,而第10行的第二个循环只增加了0.4MB,第12行的第三个循环增加了0.8MB。最后,当删除a,b和C列表时,为a和c释放了0.6MB,但是为b添加了0.2MB。对于这些结果我并不是特别理解。

总结

CPython为它的对象使用了大量内存,也使用了各种技巧和优化方式来进行内存管理。通过跟踪对象的内存使用情况并了解内存管理模型,可以显著减少程序的内存占用。

学习Python,无论你是刚入门的新手还是经验丰富的编码人员,都可以使用我们的完整Python教程指南来学习。

原文标题:

Understand How Much Memory Your Python Objects Use

原文链接:

https://code.tutsplus.com/tutorials/understand-how-much-memory-your-python-objects-use--cms-25609

编辑:王菁

校对:林亦霖

译者简介

吴振东,法国洛林大学计算机与决策专业硕士。现从事人工智能和大数据相关工作,以成为数据科学家为终生奋斗目标。来自山东济南,不会开挖掘机,但写得了Java、Python和PPT。

翻译组招募信息

工作内容:需要一颗细致的心,将选取好的外文文章翻译成流畅的中文。如果你是数据科学/统计学/计算机类的留学生,或在海外从事相关工作,或对自己外语水平有信心的朋友欢迎加入翻译小组。

你能得到:定期的翻译培训提高志愿者的翻译水平,提高对于数据科学前沿的认知,海外的朋友可以和国内技术应用发展保持联系,THU数据派产学研的背景为志愿者带来好的发展机遇。

其他福利:来自于名企的数据科学工作者,北大清华以及海外等名校学生他们都将成为你在翻译小组的伙伴。

目录
相关文章
|
5天前
|
算法 Java 程序员
Python内存管理机制深度剖析####
本文将深入浅出地探讨Python中的内存管理机制,特别是其核心组件——垃圾收集器(Garbage Collector, GC)的工作原理。不同于传统的摘要概述,我们将通过一个虚拟的故事线,跟随“内存块”小M的一生,从诞生、使用到最终被回收的过程,来揭示Python是如何处理对象生命周期,确保高效利用系统资源的。 ####
11 1
|
19天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
45 4
|
2月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
72 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
22天前
|
安全 开发者 Python
Python的内存管理pymalloc
Python的内存管理pymalloc
|
26天前
|
安全 开发者 Python
Python的内存管理pymalloc
Python的内存管理pymalloc
|
27天前
|
监控 Java API
Python是如何实现内存管理的
Python是如何实现内存管理的
|
2月前
|
Java 测试技术 Android开发
让星星⭐月亮告诉你,强软弱虚引用类型对象在内存足够和内存不足的情况下,面对System.gc()时,被回收情况如何?
本文介绍了Java中四种引用类型(强引用、软引用、弱引用、虚引用)的特点及行为,并通过示例代码展示了在内存充足和不足情况下这些引用类型的不同表现。文中提供了详细的测试方法和步骤,帮助理解不同引用类型在垃圾回收机制中的作用。测试环境为Eclipse + JDK1.8,需配置JVM运行参数以限制内存使用。
32 2
|
2月前
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
57 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
|
2月前
|
存储 Java
深入理解java对象的内存布局
这篇文章深入探讨了Java对象在HotSpot虚拟机中的内存布局,包括对象头、实例数据和对齐填充三个部分,以及对象头中包含的运行时数据和类型指针等详细信息。
29 0
深入理解java对象的内存布局
|
2月前
|
Python
深入解析 Python 中的对象创建与初始化:__new__ 与 __init__ 方法
深入解析 Python 中的对象创建与初始化:__new__ 与 __init__ 方法
19 1