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__是指向该对象的所谓弱引用列表的链接, __weakref__字段是指向包含实例属性值的类的实例字典的链接(请注意,在64位平台上的链接占用8个字节)。 从Python 3.3开始,该类的所有实例都使用一个共享的字典键空间。 这样可以减少内存中实例跟踪的大小:


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

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


份数迹线大小
1,000,000168兆字节
10,000,0001.68 GB
1亿16.8 GB

不难发现,由于实例字典的大小,内存中实例的踪迹仍然很大。


具有__slots__的类的实例


通过消除__dict____weakref__ ,可以大大减少实例在内存中的__weakref__ 。 这可以通过带有__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 

内存中的跟踪变得更加紧凑:


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

在类定义中使用__slots__导致以下事实:显着减少了内存中大量实例的跟踪:


份数迹线大小
1,000,00064兆
10,000,000640 Mb
1亿6.4 GB

当前,这是显着减少程序存储器中类实例跟踪的主要方法。


这种减少是通过以下事实实现的:在对象标头之后,对对象的引用存储在内存中,并使用类字典中的特殊描述符执行对对象的访问:


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

有一个__slots__库可以自动使用__slots__创建类。 namedlist.namedlist函数创建一个与__slots__相同的类结构:


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

另一个attrs软件包使您可以自动创建带有和不带有__slots__类。


元组


Python还具有用于表示数据集的内置tuple类型。 元组是固定的结构或记录,但没有字段名称。 要访问该字段,请使用字段索引。 元组字段在实例化时一劳永逸地与值对象相关联:


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

元组实例非常紧凑:


 >>> 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_y(self): return self[2] # def __new__(cls, x, y, z): return tuple.__new__(cls, (x, y, z)) 

此类的所有实例在内存中的跟踪都与元组相同。 大量实例留下的内存占用量略大:


份数迹线大小
1,000,00072兆
10,000,000720 Mb

记录类:不带GC的变异namedtuple


由于tuple和相应的namedtuple类会生成不可ob.x对象,也就是说ob.x值对象ob.x再与另一个值对象相关联,因此提出了对已ob.x变量进行变异的请求。 由于Python没有与支持分配的元组相同的内置类型,因此创建了许多变体。 我们将重点关注recordclass该类获得了stackoverflow评级。 另外,借助于其帮助,与tuple类型的对象的迹线的大小相比,可以减小存储器中对象的迹线的大小。


recordclass包中,引入了recordclass.mutabletuple类型 ,它与tuple几乎相同,但也支持赋值。 在此基础上,创建的子类几乎与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中正确使用它们不会生成循环引用。 因此,由默认PyGC_Head生成的类的实例的跟踪PyGC_Head片段,这对于支持循环垃圾收集机制的类是必需的(更确切地说: PyTypeObject标志未在与创建的类相对应的PyTypeObject结构的flags字段中设置)。


大量实例的跟踪大小小于具有__slots__的类的实例的跟踪大小:


份数迹线大小
1,000,00048兆
10,000,000480兆位
1亿4.8 GB

数据对象


recordclass类库中提出的另一种解决方案基于以下想法:使用内存中的存储结构,例如具有__slots__的类的实例,但不参与循环垃圾回收机制。 使用recordclass.make_dataclass函数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的方法 。 它的优点是字段可以采用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-CN455722/


All Articles