作者:Gigi Sayfan
翻译:吴振东
校对:吴金笛
本文约3300字,建议阅读10分钟。
本文将介绍多种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数据派产学研的背景为志愿者带来好的发展机遇。
其他福利:来自于名企的数据科学工作者,北大清华以及海外等名校学生他们都将成为你在翻译小组的伙伴。
点击文末“阅读原文”加入数据派团队~
转载须知
如需转载,请在开篇显著位置注明作者和出处(转自:数据派ID:datapi),并在文章结尾放置数据派醒目二维码。有原创标识文章,请发送【文章名称-待授权公众号名称及ID】至联系邮箱,申请白名单授权并按要求编辑。
发布后请将链接反馈至联系邮箱(见下方)。未经许可的转载以及改编者,我们将依法追究其法律责任。
点击“阅读原文”拥抱组织