你写的 Python 代码可以更“瘦”

举报
Python爱好者 发表于 2020/12/29 00:58:31 2020/12/29
【摘要】 作者:intellimath  译者 :弯月,责编:郭芮 出品:CSDN 在执行程序时,如果内存中有大量活动的对象,就可能出现内存问题,尤其是在可用内存总量有限的情况下。在本文中,我们将讨论缩小对象的方法,大幅减少 Python 所需的内存。 为了简便起见,我们以一个表示点的 Python 结构为例,它包括 x、y、z 坐标值,坐标值可以通过名称访问。 Dic...

作者:intellimath  译者 :弯月,责编:郭芮 出品:CSDN

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

为了简便起见,我们以一个表示点的 Python 结构为例,它包括 x、y、z 坐标值,坐标值可以通过名称访问。

Dict

在小型程序中,特别是在脚本中,使用 Python 自带的 dict 来表示结构信息非常简单方便:


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

由于在 Python 3.6 中 dict 的实现采用了一组有序键,因此其结构更为紧凑,更深得人心。但是,让我们看看 dict 在内容中占用的空间大小:


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

如上所示,dict 占用了大量内存,尤其是如果突然虚需要创建大量实例时:

实例数

对象大小

1 000 000

240 Mb

10 000 000

2.40 Gb

100 000 000

24 Gb

类实例

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


   
  1. class Point:
  2.     #
  3.     def __init__(selfxyz):
  4.         self.x = x
  5.         self.y = y
  6.         self.z = z
  7. >>> ob = Point(1,2,3)
  8. >>> x = ob.x
  9. >>> ob.y = y

类实例的结构很有趣:

字段

大小(比特)

PyGC_Head

24

PyObject_HEAD

16

__weakref__

8

__dict__

8

合计:

56

在上表中,__weakref__ 是该列表的引用,称之为到该对象的弱引用(weak reference);字段 __dict__ 是该类的实例字典的引用,其中包含实例属性的值(注意在 64-bit 引用平台中占用 8 字节)。从 Python 3.3 开始,所有类实例的字典的键都存储在共享空间中。这样就减少了内存中实例的大小:


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

因此,大量类实例在内存中占用的空间少于常规字典(dict):

实例数

大小

1 000 000

168 Mb

10 000 000

1.68 Gb

100 000 000

16.8 Gb

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

带有 __slots__ 的类实例

为了大幅降低内存中类实例的大小,我们可以考虑干掉 __dict__ 和__weakref__。为此,我们可以借助 __slots__:


   
  1. class Point:
  2.     __slots__ = 'x', 'y', 'z'
  3.     def __init__(selfxyz):
  4.         self.x = x
  5.         self.y = y
  6.         self.z = z
  7. >>> ob = Point(1,2,3)
  8. >>> print(sys.getsizeof(ob))
  9. 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

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

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


   
  1. >>> pprint(Point.__dict__)
  2. mappingproxy(
  3.               ....................................
  4.               'x': <member 'x' of 'Point' objects>,
  5.               'y': <member 'y' of 'Point' objects>,
  6.               '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)类型,代表不可修改的数据结构。元组是固定的结构或记录,但它不包含字段名称。你可以利用字段索引访问元组的字段。在创建元组实例时,元组的字段会一次性关联到值对象:


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

元组实例非常紧凑:


   
  1. >>> print(sys.getsizeof(ob))
  2. 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'))

  

如上代码创建了元组的子类,其中还定义了通过名称访问字段的描述符。对于上述示例,访问方式如下:


   
  1.  class Point(tuple):
  2.      #
  3.      @property
  4.      def _get_x(self):
  5.          return self[0]
  6.      @property
  7.      def _get_y(self):
  8.          return self[1]
  9.      @property
  10.      def _get_z(self):
  11.          return self[2]
  12.      #
  13.      def __new__(clsxyz):
  14.          return tuple.__new__(cls, (xyz))

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

实例数

大小

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 函数类似,可以自动创建这些类:


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

类实例的结构也类似于 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 继承:


   
  1. class Point(dataobject):
  2.     x:int
  3.     y:int
  4.     z:int

这种方法创建的类实例不会参与循环垃圾回收机制。内存中实例的结构与带有 __slots__ 的类相同,但没有 PyGC_Head:

字段

大小(字节)

PyObject_HEAD

16

ob_size

8

x

8

y

8

z

8

总计:

48


   
  1. >>> ob = Point(1,2,3)
  2. >>> print(sys.getsizeof(ob))
  3. 40

如果想访问字段,则需要使用特殊的描述符来表示从对象开头算起的偏移量,其位置位于类字典内:


   
  1. mappingproxy({'__new__': <staticmethod at 0x7f203c4e6be0>,
  2.               .......................................
  3.               'x': <recordclass.dataobject.dataslotgetset at 0x7f203c55c690>,
  4.               'y': <recordclass.dataobject.dataslotgetset at 0x7f203c55c670>,
  5.               '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 创建。例如:


   
  1. cdef class Python:
  2.     cdef public int xyz
  3.  def __init__(selfxyz):
  4.       self.x = x
  5.       self.y = y
  6.       self.z = z

本例中实例占用的内存更小:


   
  1. >>> ob = Point(1,2,3)
  2. >>> print(sys.getsizeof(ob))
  3. 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 包提供的函数。


   
  1. >>> 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 值之间的转换。如果从生成的数组中获取一行结果,其中包含一个元素,其内存就没那么紧凑了:


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

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

总结

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

原文:https://habr.com/en/post/458518


   
  1. 扫一扫下面的二维码免费领取10G的Python学习资料哦~
  2. “扫一扫,领取Python学习资料”

文章来源: blog.csdn.net,作者:敲代码的灰太狼,版权归原作者所有,如需转载,请联系作者。

原文链接:blog.csdn.net/tongtongjing1765/article/details/107502710

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。