如何降低 Python 的内存消耗量?

系统 1673 0
原文链接: https://data.newrank.cn/m/s.html?s=PigpOzA/LTE%3D

640?wx_fmt=gif

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

如何降低 Python 的内存消耗量?_第1张图片

作者 |  intellimath

译者 | 弯月,责编 | 郭芮

出品 | CSDN(ID:CSDNnews)

以下为译文:

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

 

640?wx_fmt=png

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

 

640?wx_fmt=png

类实例

 

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

            
              
                
                  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字节)。从Python3.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

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

 

640?wx_fmt=png

带有__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'
              
                '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__都可以利用这个包自动创建类。

 

640?wx_fmt=png

元组

 

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

 

640?wx_fmt=png

命名元组

 

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

 

640?wx_fmt=png

记录类:不带循环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

 

640?wx_fmt=png

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__'
              
                0x7f203c4e6be0>,
              .......................................
              
                
                  'x'
                
                  0x7f203c55c690>,
              
                  
                    'y'
                  
                    0x7f203c55c670>,
              
                    
                      'z'
                    
                      0x7f203c55c410>})
                    
                  
                
              
            
          

大量实例占用的内存量在CPython实现中是最小的:

实例数

大小

1 000 000

40 Mb

10 000 000

400 Mb

100 000 000

4.0 Gb

 

640?wx_fmt=png

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对象之间的转换。

 

640?wx_fmt=png

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
            
          

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

 

640?wx_fmt=png

总结

 

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

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

本文为 CSDN 翻译,转载请注明来源出处。

学Python想要达到大牛高度,你得这么学!

https://edu.csdn.net/topic/python115?utm_source=csdn_bw

【END】

如何降低 Python 的内存消耗量?_第2张图片

 热 文  推 荐 

“5 年内,PC 或将逐渐消失!”| 人物志

直接拿来用!GitHub 标星 5000+,学生党学编程有这份资料就够了

华人学者解开计算机领域 30 年难题:布尔函数敏感度猜想

☞真实揭秘 90 后程序员奔三准备:有人学金融投资,有人想当全栈工程师!

☞天网恢恢!又一名暗网比特币洗钱者被抓了

☞乘势而起,走进2019年风口“边缘计算”

☞Python之父新发文,将替换现有解析器

☞超全!深度学习在计算机视觉领域的应用一览

☞中国第一程序员,微软得不到他就要毁了他!

如何降低 Python 的内存消耗量?_第3张图片 点击阅读原文,输入关键词,即可搜索您想要的 CSDN 文章。

640?wx_fmt=png 你点的每个“在看”,我都认真当成了喜欢


更多文章、技术交流、商务合作、联系博主

微信扫码或搜索:z360901061

微信扫一扫加我为好友

QQ号联系: 360901061

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描下面二维码支持博主2元、5元、10元、20元等您想捐的金额吧,狠狠点击下面给点支持吧,站长非常感激您!手机微信长按不能支付解决办法:请将微信支付二维码保存到相册,切换到微信,然后点击微信右上角扫一扫功能,选择支付二维码完成支付。

【本文对您有帮助就好】

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描上面二维码支持博主2元、5元、10元、自定义金额等您想捐的金额吧,站长会非常 感谢您的哦!!!

发表我的评论
最新评论 总共0条评论