当程序执行期间在RAM中有大量对象处于活动状态时,尤其是在可用内存总量受到限制的情况下,可能会出现内存问题。
以下是减少对象大小的一些方法的概述,这些方法可以显着减少纯Python程序所需的RAM数量。
注意: 这是我原始帖子的英文版本(俄语)。
为简单起见,我们将考虑使用Python中的结构来表示具有坐标x
, y
, z
点, 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
这会占用大量内存,尤其是当您突然需要创建大量实例时:
类实例
对于喜欢将所有东西都穿上衣服的人,最好将结构定义为可以通过属性名称访问的类:
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
类实例的结构很有趣:
这里__weakref__
是对该对象的所谓弱引用列表的引用,字段__dict__
是对类实例字典的引用,该类实例字典包含实例属性的值(请注意,64位引用平台占用8个字节)。 从Python 3.3开始,共享空间用于在类的所有实例的字典中存储键。 这样可以减小RAM中实例跟踪的大小:
>>> print(sys.getsizeof(ob), sys.getsizeof(ob.__dict__)) 56 112
结果,大量的类实例在内存中的占用空间比常规字典( dict
)小:
不难看出,由于实例字典的大小,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中的对象大小已大大缩小:
在类定义中使用__slots__
可以显着减少内存中大量实例的占用空间:
当前,这是实质上减少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个字节,因为内存中的元组跟踪还包含许多字段:
元组
由于元组的使用非常广泛,因此有一天,有人要求您仍然可以访问字段,也可以按名称访问。 该请求的答案是模块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))
这些类的所有实例的内存占用量都与元组相同。 大量实例留下的内存占用量略大:
记录类:不带循环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
:
默认情况下, recordclass
函数创建一个不参与循环垃圾收集机制的类。 通常, recordclass
和recordclass
用于生成表示记录或简单(非递归)数据结构的类。 在Python中正确使用它们不会生成循环引用。 出于这个原因, default, the
由recordclass
生成的类的实例唤醒后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__
的类的实例的内存占用空间:
数据对象
记录类库中提出的另一种解决方案基于以下想法:在内存中使用与具有__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
:
>>> 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,大量实例的内存占用量是最小的:
赛顿
有一种基于[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
内存中的实例跟踪具有以下结构:
大量副本的占用空间较小:
但是,应该记住,从Python代码访问时,每次都会执行从int
到Python对象的转换,反之亦然。
脾气暴躁的
对大量数据使用多维数组或记录数组可提高内存利用率。 但是,为了在纯Python中进行高效处理,您应该使用专注于numpy
包中函数使用的处理方法。
>>> Point = numpy.dtype(('x', numpy.int32), ('y', numpy.int32), ('z', numpy.int32)])
使用以下函数创建由零初始化的N
元素的数组:
>>> points = numpy.zeros(N, dtype=Point)
内存中数组的大小是最小的:
对数组元素和行的常规访问将需要从Python对象转换为C int
值,反之亦然。 提取单个行将导致创建包含单个元素的数组。 它的踪迹不再那么紧凑:
>>> sys.getsizeof(points[0]) 68
因此,如上所述,在Python代码中,有必要使用numpy
包中的函数处理数组。
结论
在一个简单明了的示例中,可以验证开发人员和用户的Python编程语言(CPython)社区是否确实有可能大幅减少对象使用的内存量。