Python占用大量内存或如何减少对象的大小?

当程序执行期间在RAM中有大量对象处于活动状态时,尤其是在可用内存总量受到限制的情况下,可能会出现内存问题。


以下是减少对象大小的一些方法的概述,这些方法可以显着减少纯Python程序所需的RAM数量。


注意: 这是我原始帖子的英文版本(俄语)。


为简单起见,我们将考虑使用Python中的结构来表示具有坐标xyz点, z名称访问坐标值。


辞典


在小型程序中,尤其是在脚本中,使用内置的dict表示结构信息非常简单方便:


 >>> ob = {'x':1, 'y':2, 'z':3} >>> x = ob['x'] >>> ob['y'] = y 

随着在Python 3.6中使用一组有序键的更紧凑的实现的出现, dict变得更加有吸引力。 但是,让我们看一下它在RAM中的占用空间大小:


 >>> print(sys.getsizeof(ob)) 240 

这会占用大量内存,尤其是当您突然需要创建大量实例时:


实例数物件大小
1,000,000240兆
10,000,0002.40 Gb
1亿24 GB

类实例


对于喜欢将所有东西都穿上衣服的人,最好将结构定义为可以通过属性名称访问的类:


 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_Head24
PyObject_HEAD16
__弱引用__8
__dict__8
总计:56

这里__weakref__是对该对象的所谓弱引用列表的引用,字段__dict__是对类实例字典的引用,该类实例字典包含实例属性的值(请注意,64位引用平台占用8个字节)。 从Python 3.3开始,共享空间用于在类的所有实例的字典中存储键。 这样可以减小RAM中实例跟踪的大小:


 >>> print(sys.getsizeof(ob), sys.getsizeof(ob.__dict__)) 56 112 

结果,大量的类实例在内存中的占用空间比常规字典( dict )小:


实例数尺码
1,000,000168兆字节
10,000,0001.68 Gb
1亿16.8 Gb

不难看出,由于实例字典的大小,RAM中实例的大小仍然很大。


具有__slots__的类的实例


通过消除__dict____weakref__ ,可以显着减少RAM中类实例的大小。 这可以通过带有__slots__的“技巧”来实现:


 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 

RAM中的对象大小已大大缩小:


田野大小(字节)
PyGC_Head24
PyObject_HEAD16
X8
ÿ8
ž8
总计:64

在类定义中使用__slots__可以显着减少内存中大量实例的占用空间:


实例数尺码
1,000,00064兆
10,000,000640 Mb
1亿6.4 Gb

当前,这是实质上减少RAM中某个类实例的内存占用量的主要方法。


这种减少是通过以下事实实现的:在对象标题之后的存储器中,存储了对象引用-属性值,并使用类字典中的特殊描述符执行对它们的访问:


 >>> pprint(Point.__dict__) mappingproxy( .................................... 'x': <member 'x' of 'Point' objects>, 'y': <member 'y' of 'Point' objects>, 'z': <member 'z' of 'Point' objects>}) 

为了自动化使用__slots__创建类的过程,有一个库[namedlist]( https://pypi.org/project/namedlist )。 namedlist.namedlist函数使用__slots__创建一个类:


 >>> Point = namedlist('Point', ('x', 'y', 'z')) 

另一个软件包[attrs]( https://pypi.org/project/attrs )允许您自动创建带有和不带有__slots__


元组


Python还具有一个内置的tuple用于表示不可变的数据结构。 元组是固定的结构或记录,但没有字段名称。 对于字段访问,使用字段索引。 创建元组实例时,元组字段将与值对象永久关联:


 >>> ob = (1,2,3) >>> x = ob[0] >>> ob[1] = y # ERROR 

元组的实例非常紧凑:


 >>> print(sys.getsizeof(ob)) 72 

它们在内存中比带有__slots__的类的实例多占用8个字节,因为内存中的元组跟踪还包含许多字段:


田野大小(字节)
PyGC_Head24
PyObject_HEAD16
ob_size8
[0]8
[1]8
[2]8
总计:72

元组


由于元组的使用非常广泛,因此有一天,有人要求您仍然可以访问字段,也可以按名称访问。 该请求的答案是模块collections.namedtuple


namedtuple函数旨在自动生成此类:


 >>> Point = namedtuple('Point', ('x', 'y', 'z')) 

它创建一个元组的子类,其中定义了描述符,用于按名称访问字段。 对于我们的示例,它看起来像这样:


  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,00072兆
10,000,000720 Mb
1亿7.2 Gb

记录类:不带循环GC的可变namedtuple


由于tuple以及相应的namedtuple类生成了不可变的对象,因此属性ob.x不再可以与另一个值对象相关联,因此提出了对可变的namedtuple变体的请求。 由于Python中没有与支持分配的元组相同的内置类型,因此创建了许多选项。 我们将重点关注[recordclass]( https://pypi.org/project/recordclass ),它获得了[stackoverflow]( https://stackoverflow.com/questions/29290359/existence-of-mutable-named-元组 -python / 29419745)。 另外,与tuple的对象相比,它可以减少RAM中对象的大小。


recordclass引入了recordclass.mutabletuple类型,该类型与元组几乎相同,但也支持赋值。 在此基础上,创建的子类几乎与namedtuples完全相同,但也支持将新值分配给字段(无需创建新实例)。 与recordclass函数一样, recordclass函数使您可以自动创建以下类:


  >>> Point = recordclass('Point', ('x', 'y', 'z')) >>> ob = Point(1, 2, 3) 

类实例具有与tuple相同的结构,但仅不PyGC_Head


田野大小(字节)
PyObject_HEAD16
ob_size8
X8
ÿ8
ÿ8
总计:48

默认情况下, recordclass函数创建一个不参与循环垃圾收集机制的类。 通常, recordclassrecordclass用于生成表示记录或简单(非递归)数据结构的类。 在Python中正确使用它们不会生成循环引用。 出于这个原因, default, therecordclass生成的类的实例唤醒后default, the PyGC_Head fragment is excluded, which is necessary for classes supporting the cyclic garbage collection mechanism (more precisely: in the PyTypeObject structure, corresponding to the created class, in the标志field, by default, the flag未设置field, by default, the flag Py_TPFLAGS_HAVE_GC`)。


大量实例的内存占用空间小于具有__slots__的类的实例的内存占用空间:


实例数尺码
1,000,00048兆
10,000,000480兆位
1亿4.8 Gb

数据对象


记录类库中提出的另一种解决方案基于以下想法:在内存中使用与具有__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_HEAD16
X8
ÿ8
ÿ8
总计:40

 >>> ob = Point(1,2,3) >>> print(sys.getsizeof(ob)) 40 

要访问这些字段,还可以使用特殊的描述符来访问该字段,该描述符与对象开头之间的偏移量位于类字典中:


 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,00040兆
10,000,000400兆
1亿4.0 Gb

赛顿


有一种基于[Cython]( https://cython.org )使用的方法。 它的优点是这些字段可以采用C语言原子类型的值。 将自动创建用于从纯Python访问字段的描述符。 例如:


 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_HEAD16
X4
ÿ4
ÿ4
是空的4
总计:32

大量副本的占用空间较小:


编号尺码
1,000,00032兆位
10,000,000320兆
1亿3.2 Gb

但是,应该记住,从Python代码访问时,每次都会执行从int到Python对象的转换,反之亦然。


脾气暴躁的


对大量数据使用多维数组或记录数组可提高内存利用率。 但是,为了在纯Python中进行高效处理,您应该使用专注于numpy包中函数使用的处理方法。


 >>> Point = numpy.dtype(('x', numpy.int32), ('y', numpy.int32), ('z', numpy.int32)]) 

使用以下函数创建由零初始化的N元素的数组:


  >>> points = numpy.zeros(N, dtype=Point) 

内存中数组的大小是最小的:


物件数量尺码
1,000,00012兆
10,000,000120兆字节
1亿1.20 Gb

对数组元素和行的常规访问将需要从Python对象转换为C int值,反之亦然。 提取单个行将导致创建包含单个元素的数组。 它的踪迹不再那么紧凑:


  >>> sys.getsizeof(points[0]) 68 

因此,如上所述,在Python代码中,有必要使用numpy包中的函数处理数组。


结论


在一个简单明了的示例中,可以验证开发人员和用户的Python编程语言(CPython)社区是否确实有可能大幅减少对象使用的内存量。

Source: https://habr.com/ru/post/zh-CN458518/


All Articles