在执行程序时,若是内存中有大量活动的对象,就可能出现内存问题,尤为是在可用内存总量有限的状况下。在本文中,咱们将讨论缩小对象的方法,大幅减小 Python 所需的内存。python
为了简便起见,咱们以一个表示点的 Python 结构为例,它包括 x、y、z 坐标值,坐标值能够经过名称访问。 Dict 在小型程序中,特别是在脚本中,使用 Python 自带的 dict 来表示结构信息很是简单方便:数组
>>> ob = {'x':1, 'y':2, 'z':3}
>>> x = ob['x']
>>> ob['y'] = y
复制代码
因为在 Python 3.6 中 dict 的实现采用了一组有序键,所以其结构更为紧凑,更深得人心。可是,让咱们看看 dict 在内容中占用的空间大小:数据结构
>>> print(sys.getsizeof(ob))
240
复制代码
如上所示,dict 占用了大量内存,尤为是若是忽然虚须要建立大量实例时:app
实例数 | 对象大小 |
---|---|
1 000 000 | 240 Mb |
10 000 000 | 2.40 Gb |
100 000 000 | 24 Gb |
类实例 有些人但愿将全部东西都封装到类中,他们更喜欢将结构定义为能够经过属性名访问的类:函数
''' 遇到问题没人解答?小编建立了一个Python学习交流QQ群:857662006 寻找有志同道合的小伙伴, 互帮互助,群里还有不错的视频学习教程和PDF电子书! '''
class Point:
#
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
>>> ob = Point(1,2,3)
>>> x = ob.x
>>> ob.y = y
复制代码
类实例的结构颇有趣:学习
字段 | 大小(比特) |
---|---|
PyGC_Head | 24 |
PyObject_HEAD | 16 |
__ weakref__ | 8 |
__ dict__ | 8 |
合计: | 56 |
在上表中,__ weakref__ 是该列表的引用,称之为到该对象的弱引用(weak reference);字段 __ dict__ 是该类的实例字典的引用,其中包含实例属性的值(注意在 64-bit 引用平台中占用 8 字节)。从 Python 3.3 开始,全部类实例的字典的键都存储在共享空间中。这样就减小了内存中实例的大小:spa
>>> print(sys.getsizeof(ob), sys.getsizeof(ob.__dict__))
56 112
复制代码
所以,大量类实例在内存中占用的空间少于常规字典(dict):code
实例数 | 大小 |
---|---|
1 000 000 | 168 Mb |
10 000 000 | 1.68 Gb |
100 000 000 | 16.8 Gb |
不难看出,因为实例的字典很大,因此实例依然占用了大量内存。视频
带有 __ slots__ 的类实例对象
为了大幅下降内存中类实例的大小,咱们能够考虑干掉 __ dict__ 和__weakref__。为此,咱们能够借助 __ slots__:
''' 遇到问题没人解答?小编建立了一个Python学习交流QQ群:857662006 寻找有志同道合的小伙伴, 互帮互助,群里还有不错的视频学习教程和PDF电子书! '''
class Point:
__slots__ = 'x', 'y', 'z'
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
>>> ob = Point(1,2,3)
>>> print(sys.getsizeof(ob))
64
复制代码
如此一来,内存中的对象就明显变小了:
字段 | 大小(比特) |
---|---|
PyGC_Head | 24 |
PyObject_HEAD | 16 |
x | 8 |
y | 8 |
z | 8 |
总计: | 64 |
在类的定义中使用了 slots 之后,大量实例占据的内存就明显减小了:
实例数 | 大小 |
---|---|
1 000 000 | 64 Mb |
10 000 000 | 640 Mb |
100 000 000 | 6.4 Gb |
目前,这是下降类实例占用内存的主要方式。 这种方式减小内存的原理为:在内存中,对象的标题后面存储的是对象的引用(即属性值),访问这些属性值可使用类字典中的特殊描述符:
''' 遇到问题没人解答?小编建立了一个Python学习交流QQ群:857662006 寻找有志同道合的小伙伴, 互帮互助,群里还有不错的视频学习教程和PDF电子书! '''
>>> pprint(Point.__dict__)
mappingproxy(
....................................
'x': <member 'x' of 'Point' objects>,
'y': <member 'y' of 'Point' objects>,
'z': <member 'z' of 'Point' objects>})
复制代码
为了自动化使用 __ slots__ 建立类的过程,你可使用库namedlist(pypi.org/project/nam… 函数能够建立带有 __ slots__ 的类:
>>> Point = namedlist('Point', ('x', 'y', 'z'))
复制代码
还有一个包 attrs(pypi.org/project/att… __ slots__ 均可以利用这个包自动建立类。
元组
Python 还有一个自带的元组(tuple)类型,表明不可修改的数据结构。元组是固定的结构或记录,但它不包含字段名称。你能够利用字段索引访问元组的字段。在建立元组实例时,元组的字段会一次性关联到值对象:
''' 遇到问题没人解答?小编建立了一个Python学习交流QQ群:857662006 寻找有志同道合的小伙伴, 互帮互助,群里还有不错的视频学习教程和PDF电子书! '''
>>> ob = (1,2,3)
>>> x = ob[0]
>>> ob[1] = y # ERROR
复制代码
元组实例很是紧凑:
>>> print(sys.getsizeof(ob))
72
复制代码
因为内存中的元组还包含字段数,所以须要占据内存的 8 个字节,多于带有 slots 的类:
字段 | 大小(字节) |
---|---|
PyGC_Head | 24 |
PyObject_HEAD | 16 |
ob_size | 8 |
[0] | 8 |
[1] | 8 |
[2] | 8 |
总计: | 72 |
命名元组
因为元组的使用很是普遍,因此终有一天你须要经过名称访问元组。为了知足这种需求,你可使用模块 collections.namedtuple。 namedtuple 函数能够自动生成这种类:
>>> Point = namedtuple('Point', ('x', 'y', 'z'))
复制代码
如上代码建立了元组的子类,其中还定义了经过名称访问字段的描述符。对于上述示例,访问方式以下:
''' 遇到问题没人解答?小编建立了一个Python学习交流QQ群:857662006 寻找有志同道合的小伙伴, 互帮互助,群里还有不错的视频学习教程和PDF电子书! '''
class Point(tuple):
#
@property
def _get_x(self):
return self[0]
@property
def _get_y(self):
return self[1]
@property
def _get_z(self):
return self[2]
#
def __new__(cls, x, y, z):
return tuple.__new__(cls, (x, y, z))
复制代码
这种类全部的实例所占用的内存与元组彻底相同。但大量的实例占用的内存也会稍稍多一些:
实例数 | 大小 |
---|---|
1 000 000 | 72 Mb |
10 000 000 | 720 Mb |
100 000 000 | 7.2 Gb |
记录类:不带循环 GC 的可变动命名元组
因为元组及其相应的命名元组类可以生成不可修改的对象,所以相似于 ob.x 的对象值不能再被赋予其余值,因此有时还须要可修改的命名元组。因为 Python 没有至关于元组且支持赋值的内置类型,所以人们想了许多办法。在这里咱们讨论一下记录类(recordclass,pypi.org/project/rec… StackoverFlow 上广受好评(stackoverflow.com/questions/2…
此外,它还能够将对象占用的内存量减小到与元组对象差很少的水平。
recordclass 包引入了类型 recordclass.mutabletuple,它几乎等价于元组,但它支持赋值。它会建立几乎与 namedtuple 彻底一致的子类,但支持给属性赋新值(而不须要建立新的实例)。recordclass 函数与 namedtuple 函数相似,能够自动建立这些类:
>>> Point = recordclass('Point', ('x', 'y', 'z'))
>>> ob = Point(1, 2, 3)
复制代码
类实例的结构也相似于 tuple,但没有 PyGC_Head:
字段 | 大小(字节) |
---|---|
PyObject_HEAD | 16 |
ob_size | 8 |
x | 8 |
y | 8 |
z | 8 |
总计: | 48 |
在默认状况下,recordclass 函数会建立一个类,该类不参与垃圾回收机制。通常来讲,namedtuple 和 recordclass 均可以生成表示记录或简单数据结构(即非递归结构)的类。在 Python 中正确使用这两者不会形成循环引用。所以,recordclass 生成的类实例默认状况下不包含 PyGC_Head 片断(这个片断是支持循环垃圾回收机制的必需字段,或者更准确地说,在建立类的 PyTypeObject 结构中,flags 字段默认状况下不会设置 Py_TPFLAGS_HAVE_GC 标志)。
大量实例占用的内存量要小于带有 __ slots__ 的类实例:
实例数 | 大小 |
---|---|
1 000 000 | 48 Mb |
10 000 000 | 480 Mb |
100 000 000 | 4.8 Gb |
dataobject
recordclass 库提出的另外一个解决方案的基本想法为:内存结构采用与带 slots 的类实例一样的结构,但不参与循环垃圾回收机制。这种类能够经过 recordclass.make_dataclass 函数生成:
>>> Point = make_dataclass('Point', ('x', 'y', 'z'))
复制代码
这种方式建立的类默认会生成可修改的实例。 另外一种方法是从 recordclass.dataobject 继承:
class Point(dataobject):
x:int
y:int
z:int
复制代码
这种方法建立的类实例不会参与循环垃圾回收机制。内存中实例的结构与带有 slots 的类相同,但没有 PyGC_Head:
字段 | 大小(字节) |
---|---|
PyObject_HEAD | 16 |
ob_size | 8 |
x | 8 |
y | 8 |
z | 8 |
总计: | 48 |
>>> ob = Point(1,2,3)
>>> print(sys.getsizeof(ob))
40
复制代码
若是想访问字段,则须要使用特殊的描述符来表示从对象开头算起的偏移量,其位置位于类字典内:
''' 遇到问题没人解答?小编建立了一个Python学习交流QQ群:857662006 寻找有志同道合的小伙伴, 互帮互助,群里还有不错的视频学习教程和PDF电子书! '''
mappingproxy({'__new__': <staticmethod at 0x7f203c4e6be0>,
.......................................
'x': <recordclass.dataobject.dataslotgetset at 0x7f203c55c690>,
'y': <recordclass.dataobject.dataslotgetset at 0x7f203c55c670>,
'z': <recordclass.dataobject.dataslotgetset at 0x7f203c55c410>})
复制代码
大量实例占用的内存量在 CPython 实现中是最小的:
实例数 | 大小 |
---|---|
1 000 000 | 40 Mb |
10 000 000 | 400 Mb |
100 000 000 | 4.0 Gb |
Cython
还有一个基于 Cython(cython.org/)的方案。该方案的优势… C 语言的原子类型。访问字段的描述符能够经过纯 Python 建立。例如:
''' 遇到问题没人解答?小编建立了一个Python学习交流QQ群:857662006 寻找有志同道合的小伙伴, 互帮互助,群里还有不错的视频学习教程和PDF电子书! '''
cdef class Python:
cdef public int x, y, z
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
复制代码
本例中实例占用的内存更小:
>>> ob = Point(1,2,3)
>>> print(sys.getsizeof(ob))
32
复制代码
内存结构以下:
字段 | 大小(字节) |
---|---|
PyObject_HEAD | 16 |
x | 4 |
y | 4 |
z | 4 |
nycto | 4 |
总计: | 32 |
大量副本所占用的内存量也很小:
实例数 | 大小 |
---|---|
1 000 000 | 32 Mb |
10 000 000 | 320 Mb |
100 000 000 | 3.2 Gb |
可是,须要记住在从 Python 代码访问时,每次访问都会引起 int 类型和 Python 对象之间的转换。
Numpy
使用拥有大量数据的多维数组或记录数组会占用大量内存。可是,为了有效地利用纯 Python 处理数据,你应该使用 Numpy 包提供的函数。
>>> Point = numpy.dtype(('x', numpy.int32), ('y', numpy.int32), ('z', numpy.int32)])
复制代码
一个拥有 N 个元素、初始化成零的数组能够经过下面的函数建立:
>>> points = numpy.zeros(N, dtype=Point)
复制代码
内存占用是最小的:
实例数 | 大小 |
---|---|
1 000 000 | 12 Mb |
10 000 000 | 120 Mb |
100 000 000 | 1.2 Gb |
通常状况下,访问数组元素和行会引起 Python 对象与 C 语言 int 值之间的转换。若是从生成的数组中获取一行结果,其中包含一个元素,其内存就没那么紧凑了:
>>> sys.getsizeof(points[0])
68
复制代码
所以,如上所述,在 Python 代码中须要使用 numpy 包提供的函数来处理数组。
总结 在本文中,咱们经过一个简单明了的例子,求证了 Python 语言(CPython)社区的开发人员和用户能够真正减小对象占用的内存量。