如何降低 Python 的内存消耗量

在执行程序时,我们经常会处理大量的数据,如果内存中有大量使用的变量,就可能出现内存问题,尤其是在可用内存总量有限的情况下。在本帖子中,我们将讨论缩小对象的方法,大幅减少 Python 所需的内存。希望对大家能带来帮助。

一、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 占用了大量内存,尤其是如果突然虚需要创建大量实例时:
如何降低 Python 的内存消耗量

二、类实例

有些人希望将所有东西都封装到类中,他们更喜欢将结构定义为可以通过属性名访问的类:

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

类实例的结构的内存大小:
如何降低 Python 的内存消耗量
在上表中,__weakref__ 是该列表的引用,称之为到该对象的弱引用(weak reference);字段 __dict__ 是该类的实例字典的引用,其中包含实例属性的值。从 Python3.3 开始,所有类实例的字典的键都存储在共享空间中。这样就减少了内存中实例的大小:

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

因此,大量类实例在内存中占用的空间少于常规字典(dict):
如何降低 Python 的内存消耗量

不难看出,由于实例的字典很大,所以实例依然占用了大量内存。

三、带有 __slots__ 的类实例

为了大幅降低内存中类实例的大小,我们可以考虑干掉 __dict__ 和 __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

如此一来,内存中的对象就明显变小了:
如何降低 Python 的内存消耗量
在类的定义中使用了 __slots__ 以后,大量实例占据的内存就明显减少了:
如何降低 Python 的内存消耗量

目前,这是降低类实例占用内存的主要方式。

这种方式减少内存的原理为:在内存中,对象的标题后面存储的是对象的引用(即属性值),访问这些属性值可以使用类字典中的特殊描述符:

>>> pprint(Point.__dict__)
mappingproxy(
              ....................................
              'x': 'x' of 'Point' objects>,
              'y': 'y' of 'Point' objects>,
              'z': '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__ 都可以利用这个包自动创建类。

4. 元组

Python 还有一个自带的元组(tuple)类型,代表不可修改的数据结构。元组是固定的结构或记录,但它不包含字段名称。你可以利用字段索引访问元组的字段。在创建元组实例时,元组的字段会一次性关联到值对象:

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

元组实例非常紧凑:

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

由于内存中的元组还包含字段数,因此需要占据内存的 8 个字节,多于带有 __slots__ 的类:
如何降低 Python 的内存消耗量

五、命名元组

由于元组的使用非常广泛,所以终有一天你需要通过名称访问元组。为了满足这种需求,你可以使用模块 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))

这种类所有的实例所占用的内存与元组完全相同。但大量的实例占用的内存也会稍稍多一些:
如何降低 Python 的内存消耗量

六、Numpy

使用拥有大量数据的多维数组或记录数组会占用大量内存。但是,为了有效地利用纯 Python 处理数据,你应该使用 Numpy 包提供的函数。

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

一个拥有 N 个元素、初始化成零的数组可以通过下面的函数创建:

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

内存占用是最小的:
如何降低 Python 的内存消耗量
一般情况下,访问数组元素和行会引发 Python 对象与 C 语言 int 值之间的转换。如果从生成的数组中获取一行结果,其中包含一个元素,其内存就没那么紧凑了:

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

因此,如上所述,在 Pytho 代码中需要使用 numpy 包提供的函数来处理数组。