(点击上方快速关注并设置为星标,一起学Python)
作者 | intellimath  译者 | 弯月,责编 | 郭芮
出品 | CSDN(ID:CSDNnews)

在执行程序时,如果内存中有大量活动的对象,就可能出现内存问题,尤其是在可用内存总量有限的情况下。在本文中,我们将讨论缩小对象的方法,大幅减少 Python 所需的内存。

为了简便起见,我们以一个表示点的 Python 结构为例,它包括 x、y、z 坐标值,坐标值可以通过名称访问。
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 占用了大量内存,尤其是如果突然虚需要创建大量实例时:
实例数
对象大小
1 000 000
240 Mb
10 000 000
2.40 Gb
100 000 000
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_Head
24
PyObject_HEAD
16
__weakref__
8
__dict__
8
合计:
56
在上表中,__weakref__ 是该列表的引用,称之为到该对象的弱引用(weak reference);字段 __dict__ 是该类的实例字典的引用,其中包含实例属性的值(注意在 64-bit 引用平台中占用 8 字节)。从 Python 3.3 开始,所有类实例的字典的键都存储在共享空间中。这样就减少了内存中实例的大小:
>>> print(sys.getsizeof(ob), sys.getsizeof(ob.__dict__)) 
56 112
因此,大量类实例在内存中占用的空间少于常规字典(dict):
实例数
大小
1 000 000
168 Mb
10 000 000
1.68 Gb
100 000 000
16.8 Gb
不难看出,由于实例的字典很大,所以实例依然占用了大量内存。

带有 __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

如此一来,内存中的对象就明显变小了:
字段
大小(比特)
PyGC_Head
24
PyObject_HEAD
16
x
8
y
8
z
8
总计:
64
在类的定义中使用了 __slots__ 以后,大量实例占据的内存就明显减少了:
实例数
大小
1 000 000
64 Mb
10 000 000
640 Mb
100 000 000
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__ 创建类的过程,你可以使用库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
由于内存中的元组还包含字段数,因此需要占据内存的 8 个字节,多于带有 __slots__ 的类:
字段
大小(字节)
PyGC_Head
24
PyObject_HEAD
16
ob_size
8
[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_z(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

记录类:不带循环 GC 的可变更命名元组

由于元组及其相应的命名元组类能够生成不可修改的对象,因此类似于 ob.x 的对象值不能再被赋予其他值,所以有时还需要可修改的命名元组。由于 Python 没有相当于元组且支持赋值的内置类型,因此人们想了许多办法。在这里我们讨论一下记录类(recordclass,https://pypi.org/project/recordclass),它在 StackoverFlow 上广受好评(https://stackoverflow.com/questions/29290359/existence-of-mutable-named-tuple-in)。
此外,它还可以将对象占用的内存量减少到与元组对象差不多的水平。
recordclass 包引入了类型 recordclass.mutabletuple,它几乎等价于元组,但它支持赋值。它会创建几乎与 namedtuple 完全一致的子类,但支持给属性赋新值(而不需要创建新的实例)。recordclass 函数与 namedtuple 函数类似,可以自动创建这些类:
>>> Point = recordclass('Point', ('x', 'y', 'z'))
 >>> ob = Point(1, 2, 3)
类实例的结构也类似于 tuple,但没有 PyGC_Head:
字段
大小(字节)
PyObject_HEAD
16
ob_size
8
x
8
y
8
z
8
总计:
48
在默认情况下,recordclass 函数会创建一个类,该类不参与垃圾回收机制。一般来说,namedtuple 和 recordclass 都可以生成表示记录或简单数据结构(即非递归结构)的类。在 Python 中正确使用这二者不会造成循环引用。因此,recordclass 生成的类实例默认情况下不包含 PyGC_Head 片段(这个片段是支持循环垃圾回收机制的必需字段,或者更准确地说,在创建类的 PyTypeObject 结构中,flags 字段默认情况下不会设置 Py_TPFLAGS_HAVE_GC 标志)。
大量实例占用的内存量要小于带有 __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'))
这种方式创建的类默认会生成可修改的实例。
另一种方法是从 recordclass.dataobject 继承:
class Point(dataobject):
    x:int
    y:int
    z:int
这种方法创建的类实例不会参与循环垃圾回收机制。内存中实例的结构与带有 __slots__ 的类相同,但没有 PyGC_Head:
字段
大小(字节)
PyObject_HEAD
16
ob_size
8
x
8
y
8
z
8
总计:
48
>>> 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 000
40 Mb
10 000 000
400 Mb
100 000 000
4.0 Gb

Cython

还有一个基于 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
内存结构如下:
字段
大小(字节)
PyObject_HEAD
16
x
4
y
4
z
4
nycto
4
总计:
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)])
一个拥有 N 个元素、初始化成零的数组可以通过下面的函数创建:
>>> points = numpy.zeros(N, dtype=Point)
内存占用是最小的:
实例数
大小
1 000 000
12 Mb
10 000 000
120 Mb
100 000 000
1.2 Gb
一般情况下,访问数组元素和行会引发 Python 对象与 C 语言 int 值之间的转换。如果从生成的数组中获取一行结果,其中包含一个元素,其内存就没那么紧凑了:
>>> sys.getsizeof(points[0])
  68
因此,如上所述,在 Python 代码中需要使用 numpy 包提供的函数来处理数组。

总结

在本文中,我们通过一个简单明了的例子,求证了 Python 语言(CPython)社区的开发人员和用户可以真正减少对象占用的内存量。
原文:https://habr.com/en/post/458518

扫码 1 秒即可关注哦~

点「在看」的人都变好看了哦

你写的 Python 代码可以更“瘦”相关推荐

  1. 多元线性回归算法python实现_手写算法-Python代码推广多元线性回归

    1.梯度下降-矩阵形式 上篇文章介绍了一元线性回归,包括Python实现和sklearn实现的实例.对比,以及一些问题点,详情可以看这里: 链接: 手写算法-Python代码实现一元线性回归 里面封装 ...

  2. python函数的组成要素_python函数要素有哪些?这7点是你写好python代码的关键

    [摘要]对于python小白来说,写好一个python代码并不容易,不过你知道python函数要素有哪些?这7点是你写好python代码的关键,如果你想学好python,那么本文内容一定要自己试试,毕 ...

  3. 手写算法-python代码实现Ridge(L2正则项)回归

    手写算法-python代码实现Ridge回归 Ridge简介 Ridge回归分析与python代码实现 方法一:梯度下降法求解Ridge回归参数 方法二:标准方程法实现Ridge回归 调用sklear ...

  4. python实现tomasulo算法_手写算法-python代码实现KNN

    本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,如有问题请及时联系我们以作处理 原理解析 KNN-全称K-Nearest Neighbor,最近邻算法,可以做分类任务,也可以做回归 ...

  5. python程序写完后点哪个运行快_让 Python 代码运行更快的最佳方式!

    PyPy与CPython PyPy是Python解释器CPython的直接替代品.CPython将Python编译为中间字节码然后由虚拟机解释,而PyPy使用实时(JIT)编译将Python代码转换为 ...

  6. 写好 Python 代码的几条原则

    程序设计的好与坏,早在我们青葱岁月时就接触过了,只是那是并不知道这竟如此重要.能够立即改善程序设计.写出"好"代码的知识有以下几点: •面向对象五个基本原则: •常见的三种架构: ...

  7. 一个让Python代码运行更快的最佳方式!

    作者 | Serdar Yegulalp 译者 | 姜松浩,责编 | 屠敏 转载自 CSDN(ID:CSDNnews) Python因其强大.灵活且易于使用等特性,而赢得了声誉.这些优点使其在各种各样 ...

  8. 你写的Python代码规范吗?

    总第141篇/张俊红 1.什么是PEP8 PEP 是 Python Enhancement Proposals 的缩写,直译过来就是「Python增强建议书」也可叫做「Python改进建议书」,说的直 ...

  9. python多元非线性拟合csdn_手写算法-Python代码实现非线性回归

    生成非线性数据集 前面我们介绍了Python代码实现线性回归,今天,我们来聊一聊当数据呈现非线性时,这时我们继续用线性表达式去拟合,显然效果会很差,那我们该怎么处理?继续上实例(我们的代码里用到的数据 ...

最新文章

  1. IBM HACMP 系列 -- 后期安装工作和管理任务二
  2. 如何解决Bluetooth系统设计的棘手问题
  3. 使用HtmlAgilityPack抓取网页数据
  4. 【机器视觉学习笔记】伽马变换(C++)
  5. JS-节点增删改-document-HTML DOM-事件
  6. 外点罚函数matlab程序_关于图像轮廓识别的程序实现
  7. 做技术的你,如果别人找你创业,该怎么办?
  8. Office 2007,在编辑Word时,文件经常无法保存会丢失
  9. installshield中用release wizard打包.net framework 1.1中文版
  10. python大作业 课设
  11. 发票专用驱动sjz_增值税发票选择确认平台使用手册
  12. 怎么用计算机按log以2为底,log以2为底4的对数在计算器上怎么按?
  13. Linux之shell脚本编程、多命令、脚本、bc计算器、反引号、if语句、for语句
  14. D. Relatively Prime Graph(构造+数论)
  15. JVM-内存区域 堆、方法区,虚拟机栈、程序计数器详解
  16. 手机键盘(简单模拟)
  17. c++ 堆优化版dijkstra 代码实现
  18. mysql计算某个两个时间的差值
  19. 8.显卡芯片厂家及芯片命名规则-ATI、NVIDIA
  20. ORA-65096: 公用用户名或角色名无效

热门文章

  1. 使用git建立远程仓库,让别人git clone下来
  2. Elasticsearch和MongoDB对比
  3. MySQL中空字符串与null的区别:计数 判断 时间
  4. html 表格_UiPath之发送正文包含表格的邮件(通过Html实现表格)
  5. oc语言和c语言,C语言及OC语言初期学习心得
  6. html缓存在本地缓存,HTML5 本地缓存 window.localStorage
  7. 成田机场坐access到品川_@马大哈们:北京有三座机场了,以后订票别选错哦!大兴机场今试飞...
  8. 泛型方法的定义和使用_泛型( Generic )
  9. python读写excel模块pandas_Python3使用pandas模块读写excel操作示例
  10. 遥感计算机分类实验的难点,8-遥感实验.doc