會了這幾招,可讓你的 Python 代碼能夠更「瘦」

在執行程序時,若是內存中有大量活動的對象,就可能出現內存問題,尤爲是在可用內存總量有限的狀況下。在本文中,咱們將討論縮小對象的方法,大幅減小 Python 所需的內存。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 佔用了大量內存,尤爲是若是忽然虛須要建立大量實例時:app

實例數 對象大小
1 000 000 240 Mb
10 000 000 2.40 Gb
100 000 000 24 Gb

類實例 有些人但願將全部東西都封裝到類中,他們更喜歡將結構定義爲能夠經過屬性名訪問的類:函數

''' 遇到問題沒人解答?小編建立了一個Python學習交流QQ羣:857662006 尋找有志同道合的小夥伴, 互幫互助,羣裏還有不錯的視頻學習教程和PDF電子書! '''
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 開始,全部類實例的字典的鍵都存儲在共享空間中。這樣就減小了內存中實例的大小:spa

>>> print(sys.getsizeof(ob), sys.getsizeof(ob.__dict__)) 
56 112
複製代碼

所以,大量類實例在內存中佔用的空間少於常規字典(dict):code

實例數 大小
1 000 000 168 Mb
10 000 000 1.68 Gb
100 000 000 16.8 Gb

不難看出,因爲實例的字典很大,因此實例依然佔用了大量內存。視頻

帶有 __ slots__ 的類實例對象

爲了大幅下降內存中類實例的大小,咱們能夠考慮幹掉 __ dict__ 和__weakref__。爲此,咱們能夠藉助 __ slots__:

''' 遇到問題沒人解答?小編建立了一個Python學習交流QQ羣:857662006 尋找有志同道合的小夥伴, 互幫互助,羣裏還有不錯的視頻學習教程和PDF電子書! '''
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

目前,這是下降類實例佔用內存的主要方式。 這種方式減小內存的原理爲:在內存中,對象的標題後面存儲的是對象的引用(即屬性值),訪問這些屬性值可使用類字典中的特殊描述符:

''' 遇到問題沒人解答?小編建立了一個Python學習交流QQ羣:857662006 尋找有志同道合的小夥伴, 互幫互助,羣裏還有不錯的視頻學習教程和PDF電子書! '''
>>> pprint(Point.__dict__)
mappingproxy(
              ....................................
              'x': <member 'x' of 'Point' objects>,
              'y': <member 'y' of 'Point' objects>,
              'z': <member 'z' of 'Point' objects>})
複製代碼

爲了自動化使用 __ slots__ 建立類的過程,你可使用庫namedlist(pypi.org/project/nam… 函數能夠建立帶有 __ slots__ 的類:

>>> Point = namedlist('Point', ('x', 'y', 'z'))
複製代碼

還有一個包 attrs(pypi.org/project/att… __ slots__ 均可以利用這個包自動建立類。

元組

Python 還有一個自帶的元組(tuple)類型,表明不可修改的數據結構。元組是固定的結構或記錄,但它不包含字段名稱。你能夠利用字段索引訪問元組的字段。在建立元組實例時,元組的字段會一次性關聯到值對象:

''' 遇到問題沒人解答?小編建立了一個Python學習交流QQ羣:857662006 尋找有志同道合的小夥伴, 互幫互助,羣裏還有不錯的視頻學習教程和PDF電子書! '''
>>> 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'))
複製代碼

如上代碼建立了元組的子類,其中還定義了經過名稱訪問字段的描述符。對於上述示例,訪問方式以下:

''' 遇到問題沒人解答?小編建立了一個Python學習交流QQ羣:857662006 尋找有志同道合的小夥伴, 互幫互助,羣裏還有不錯的視頻學習教程和PDF電子書! '''
 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,pypi.org/project/rec… StackoverFlow 上廣受好評(stackoverflow.com/questions/2…

此外,它還能夠將對象佔用的內存量減小到與元組對象差很少的水平。

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
複製代碼

若是想訪問字段,則須要使用特殊的描述符來表示從對象開頭算起的偏移量,其位置位於類字典內:

''' 遇到問題沒人解答?小編建立了一個Python學習交流QQ羣:857662006 尋找有志同道合的小夥伴, 互幫互助,羣裏還有不錯的視頻學習教程和PDF電子書! '''
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(cython.org/)的方案。該方案的優勢… C 語言的原子類型。訪問字段的描述符能夠經過純 Python 建立。例如:

''' 遇到問題沒人解答?小編建立了一個Python學習交流QQ羣:857662006 尋找有志同道合的小夥伴, 互幫互助,羣裏還有不錯的視頻學習教程和PDF電子書! '''
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)社區的開發人員和用戶能夠真正減小對象佔用的內存量。

相關文章
相關標籤/搜索