Python内存优化实战:从Dict到Numpy的多种方案对比

雨后青山
雨后青山
发布时间:2026-10-13| 5人浏览过
原创

本文深入探讨Python中处理大量数据时的内存优化策略。通过对比Dict、Class、__slots__、Tuple、NamedTuple、Recordclass、Cython及Numpy等方案,展示不同数据结构在内存占用上的显著差异。文章提供了具体的代码示例和内存测试数据,帮助开发者根据场景选择最优方案,有效降低内存消耗,提升程序性能。

Python内存优化概述

概述

当程序需要处理海量或复杂数据时,运行期间的内存占用会急剧上升。一旦内存使用触及系统限制,操作系统可能会强制终止进程,这在严格限制内存配额的环境中尤为常见。本文将介绍几种有效降低Python内存占用的方法。

注:以下示例代码均基于Python 3环境运行。

案例演示

以一个简单的三维坐标存储场景为例,我们需要存储x、y、z三个坐标值。

基础数据结构内存对比

字典方案

利用Python内置的字典(Dict)数据结构可以非常便捷地实现上述需求。


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

观察对象ob的内存占用情况:


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

仅存储三个整数,内存占用却相当可观。试想若需存储海量此类数据,总内存消耗将极为巨大。

数据量 占用内存大小
1 000 000 240 Mb
10 000 000 2.40 Gb
100 000 000 24 Gb

类方案

面向对象编程爱好者倾向于将数据封装在类(Class)中。实现相同需求如下:


class Point:
 #
 def __init__(self, x, y, z):
 self.x = x
 self.y = y
 self.z = z

>>> ob = Point(1,2,3)

类的数据结构与字典存在显著差异,我们来看看此时的内存占用情况:

字段 占用内存
PyGC_Head 24
PyObject_HEAD 16
__weakref__ 8
__dict__ 8
TOTAL 56

关于__weakref__(弱引用)机制,可参考相关文档。对象的__dict__中存储了self.xxx等属性信息。自Python 3.3起,键名采用共享内存存储,从而减少了RAM中实例跟踪的大小。


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

数据量 占用内存
1 000 000 168 Mb
10 000 000 1.68 Gb
100 000 000 16.8 Gb

结果显示,类的内存占用比字典有所减少,但这一优化幅度尚显不足。

__slots__优化策略

__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

可见,内存占用已大幅降低。

字段 内存占用
PyGC_Head 24
PyObject_HEAD 16
x 8
y 8
z 8
TOTAL 64

数据量 占用内存
1 000 000 64Mb
10 000 000 640Mb
100 000 000 6.4Gb

默认情况下,Python的新式类和经典类实例均拥有一个__dict__来存储实例属性。这虽提供了极大的灵活性,允许在程序中动态添加新属性,但对于在“编译”前已知固定属性数量的小型类而言,这种__dict__机制反而造成了内存浪费。

当需要创建大量实例时,这一问题尤为突出。一种有效的解决方式是在新式类中定义__slots__属性。

__slots__声明中列出了若干实例变量,并为每个变量预留精确的空间;这样Python便不再使用__dict__,从而节省空间。

那么,使用__slots__是否绝对必要?使用__slots__也伴随着一些副作用:

  1. 每个继承的子类都必须重新定义一遍__slots__。
  2. 实例只能包含__slots__中定义的属性,这限制了编程的灵活性。例如,若因某种原因试图给instance设置新属性(如instance.a = 1),由于a不在__slots__中,程序将直接报错。你必须不断修改__slots__或采用其他迂回方式解决。
  3. 实例无法拥有弱引用(weakref)目标,若需使用weakref,必须将其显式加入__slots__。

最后,namedlist和attrs库提供了自动创建带__slots__的类的方法,感兴趣读者可以尝试。

元组与命名元组

元组方案

Python还提供了一种内置类型:元组(Tuple),用于表示不可变数据结构。元组是固定结构或记录,但没有字段名称,字段访问需通过索引。在创建元组实例时,字段值一次性与值对象关联:


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

元组的示例代码简洁明了:


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

其内存占用仅比__slots__方案多出8字节:

字段 占用内存(bytes)
PyGC_Head 24
PyObject_HEAD 16
ob_size 8
[0] 8
[1] 8
[2] 8
TOTAL 72

命名元组方案

通过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 000 72 Mb
10 000 000 720 Mb
100 000 000 7.2 Gb

Recordclass高级优化

Recordclass方案

Python第三方库recordclass提供了一个名为recordclass.mutabletuple的数据结构,其功能与内置tuple几乎一致,但占用更少的内存。


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

实例化后,仅少了PyGC_Head部分:

字段 占用内存
PyObject_HEAD 16
ob_size 8
x 8
y 8
y 8
TOTAL 48

由此可见,与__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'))

另一种方法是继承自dataobject:


class Point(dataobject):
 x:int
 y:int
 z:int

以此方式创建的类将生成不参与循环垃圾收集机制的实例。内存中实例的结构与__slots__情况相同,但无PyGC_Head:

字段 内存占用(bytes)
PyObject_HEAD 16
x 8
y 8
y 8
TOTAL 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>})

数据量 内存占用
1 000 000 40 Mb
10 000 000 400 Mb
100 000 000 4.0 Gb

Cython与Numpy方案

Cython方案

还有一种基于Cython的方法。其优势在于字段可采用C语言原子类型的值。例如:


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

内存结构分布如下:

字段 内存占用(bytes)
PyObject_HEAD 16
x 4
y 4
y 4
пусто 4
TOTAL 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)])

创建初始值为0的数组:


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

数据量 内存占用
1 000 000 12 Mb
10 000 000 120 Mb
100 000 000 1.2 Gb

总结

可以看出,在Python性能优化方面仍有诸多工作可做。Python提供了便利,但也需占用较多资源。在不同场景下,我们需要选择合适的方法,以获得更佳的性能体验。

以上就是本文的全部内容,希望本文的内容对大家的学习或工作具有一定的参考借鉴价值,感谢大家对本站的支持。

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。