在執行程序時,若是內存中有大量活動的對象,就可能出現內存問題,尤爲是在可用內存總量有限的狀況下。在本文中,咱們將討論縮小對象的方法,大幅減小 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)社區的開發人員和用戶能夠真正減小對象佔用的內存量。