Python内存优化实战:从Dict到Numpy的多种方案对比
本文深入探讨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__也伴随着一些副作用:
- 每个继承的子类都必须重新定义一遍__slots__。
- 实例只能包含__slots__中定义的属性,这限制了编程的灵活性。例如,若因某种原因试图给instance设置新属性(如instance.a = 1),由于a不在__slots__中,程序将直接报错。你必须不断修改__slots__或采用其他迂回方式解决。
- 实例无法拥有弱引用(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提供了便利,但也需占用较多资源。在不同场景下,我们需要选择合适的方法,以获得更佳的性能体验。
以上就是本文的全部内容,希望本文的内容对大家的学习或工作具有一定的参考借鉴价值,感谢大家对本站的支持。
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













