python减少内存_如何降低 Python 的内存消耗量?
👆“Python貓” ,一個(gè)值得加星標(biāo)的公眾號(hào)
在執(zhí)行程序時(shí),如果內(nèi)存中有大量活動(dòng)的對(duì)象,就可能出現(xiàn)內(nèi)存問題,尤其是在可用內(nèi)存總量有限的情況下。在本文中,我們將討論縮小對(duì)象的方法,大幅減少Python所需的內(nèi)存。
圖 | 《借東西的小人阿莉埃蒂》劇照
作者 |?intellimath
譯者 |?彎月,責(zé)編 | 郭芮
出品 | CSDN(ID:CSDNnews)
以下為譯文:
為了簡(jiǎn)便起見,我們以一個(gè)表示點(diǎn)的Python結(jié)構(gòu)為例,它包括x、y、z坐標(biāo)值,坐標(biāo)值可以通過名稱訪問。
Dict
在小型程序中,特別是在腳本中,使用Python自帶的dict來表示結(jié)構(gòu)信息非常簡(jiǎn)單方便:
>>>?ob?=?{'x':1,?'y':2,?'z':3}
>>>?x?=?ob['x']
>>>?ob['y']?=?y
由于在Python 3.6中dict的實(shí)現(xiàn)采用了一組有序鍵,因此其結(jié)構(gòu)更為緊湊,更深得人心。但是,讓我們看看dict在內(nèi)容中占用的空間大小:
>>>?print(sys.getsizeof(ob))
240
如上所示,dict占用了大量?jī)?nèi)存,尤其是如果突然虛需要?jiǎng)?chuàng)建大量實(shí)例時(shí):
實(shí)例數(shù)
對(duì)象大小
1 000 000
240 Mb
10 000 000
2.40 Gb
100 000 000
24 Gb
類實(shí)例
有些人希望將所有東西都封裝到類中,他們更喜歡將結(jié)構(gòu)定義為可以通過屬性名訪問的類:
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
類實(shí)例的結(jié)構(gòu)很有趣:
字段
大小(比特)
PyGC_Head
24
PyObject_HEAD
16
__weakref__
8
__dict__
8
合計(jì):
56
在上表中,__weakref__是該列表的引用,稱之為到該對(duì)象的弱引用(weak reference);字段__dict__是該類的實(shí)例字典的引用,其中包含實(shí)例屬性的值(注意在64-bit引用平臺(tái)中占用8字節(jié))。從Python3.3開始,所有類實(shí)例的字典的鍵都存儲(chǔ)在共享空間中。這樣就減少了內(nèi)存中實(shí)例的大小:
>>>?print(sys.getsizeof(ob),?sys.getsizeof(ob.__dict__))
56?112
因此,大量類實(shí)例在內(nèi)存中占用的空間少于常規(guī)字典(dict):
實(shí)例數(shù)
大小
1 000 000
168 Mb
10 000 000
1.68 Gb
100 000 000
16.8 Gb
不難看出,由于實(shí)例的字典很大,所以實(shí)例依然占用了大量?jī)?nèi)存。
帶有__slots__的類實(shí)例
為了大幅降低內(nèi)存中類實(shí)例的大小,我們可以考慮干掉__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
如此一來,內(nèi)存中的對(duì)象就明顯變小了:
字段
大小(比特)
PyGC_Head
24
PyObject_HEAD
16
x
8
y
8
z
8
總計(jì):
64
在類的定義中使用了__slots__以后,大量實(shí)例占據(jù)的內(nèi)存就明顯減少了:
實(shí)例數(shù)
大小
1 000 000
64 Mb
10 000 000
640 Mb
100 000 000
6.4 Gb
目前,這是降低類實(shí)例占用內(nèi)存的主要方式。
這種方式減少內(nèi)存的原理為:在內(nèi)存中,對(duì)象的標(biāo)題后面存儲(chǔ)的是對(duì)象的引用(即屬性值),訪問這些屬性值可以使用類字典中的特殊描述符:
>>>?pprint(Point.__dict__)
mappingproxy(
....................................
'x':?,
'y':?,
'z':?})
為了自動(dòng)化使用__slots__創(chuàng)建類的過程,你可以使用庫namedlist(https://pypi.org/project/namedlist)。namedlist.namedlist函數(shù)可以創(chuàng)建帶有__slots__的類:
>>>?Point?=?namedlist('Point',?('x',?'y',?'z'))
還有一個(gè)包attrs(https://pypi.org/project/attrs),無論使用或不使用__slots__都可以利用這個(gè)包自動(dòng)創(chuàng)建類。
元組
Python還有一個(gè)自帶的元組(tuple)類型,代表不可修改的數(shù)據(jù)結(jié)構(gòu)。元組是固定的結(jié)構(gòu)或記錄,但它不包含字段名稱。你可以利用字段索引訪問元組的字段。在創(chuàng)建元組實(shí)例時(shí),元組的字段會(huì)一次性關(guān)聯(lián)到值對(duì)象:
>>>?ob?=?(1,2,3)
>>>?x?=?ob[0]
>>>?ob[1]?=?y?#?ERROR
元組實(shí)例非常緊湊:
>>>?print(sys.getsizeof(ob))
72
由于內(nèi)存中的元組還包含字段數(shù),因此需要占據(jù)內(nèi)存的8個(gè)字節(jié),多于帶有__slots__的類:
字段
大小(字節(jié))
PyGC_Head
24
PyObject_HEAD
16
ob_size
8
[0]
8
[1]
8
[2]
8
總計(jì):
72
命名元組
由于元組的使用非常廣泛,所以終有一天你需要通過名稱訪問元組。為了滿足這種需求,你可以使用模塊collections.namedtuple。
namedtuple函數(shù)可以自動(dòng)生成這種類:
>>>?Point?=?namedtuple('Point',?('x',?'y',?'z'))
如上代碼創(chuàng)建了元組的子類,其中還定義了通過名稱訪問字段的描述符。對(duì)于上述示例,訪問方式如下:
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))
這種類所有的實(shí)例所占用的內(nèi)存與元組完全相同。但大量的實(shí)例占用的內(nèi)存也會(huì)稍稍多一些:
實(shí)例數(shù)
大小
1 000 000
72 Mb
10 000 000
720 Mb
100 000 000
7.2 Gb
記錄類:不帶循環(huán)GC的可變更命名元組
由于元組及其相應(yīng)的命名元組類能夠生成不可修改的對(duì)象,因此類似于ob.x的對(duì)象值不能再被賦予其他值,所以有時(shí)還需要可修改的命名元組。由于Python沒有相當(dāng)于元組且支持賦值的內(nèi)置類型,因此人們想了許多辦法。在這里我們討論一下記錄類(recordclass,https://pypi.org/project/recordclass),它在StackoverFlow上廣受好評(píng)(https://stackoverflow.com/questions/29290359/existence-of-mutable-named-tuple-in)。
此外,它還可以將對(duì)象占用的內(nèi)存量減少到與元組對(duì)象差不多的水平。
recordclass包引入了類型recordclass.mutabletuple,它幾乎等價(jià)于元組,但它支持賦值。它會(huì)創(chuàng)建幾乎與namedtuple完全一致的子類,但支持給屬性賦新值(而不需要?jiǎng)?chuàng)建新的實(shí)例)。recordclass函數(shù)與namedtuple函數(shù)類似,可以自動(dòng)創(chuàng)建這些類:
>>>Point?=?recordclass('Point',?('x',?'y',?'z'))
>>>ob?=?Point(1,?2,?3)
類實(shí)例的結(jié)構(gòu)也類似于tuple,但沒有PyGC_Head:
字段
大小(字節(jié))
PyObject_HEAD
16
ob_size
8
x
8
y
8
z
8
總計(jì):
48
在默認(rèn)情況下,recordclass函數(shù)會(huì)創(chuàng)建一個(gè)類,該類不參與垃圾回收機(jī)制。一般來說,namedtuple和recordclass都可以生成表示記錄或簡(jiǎn)單數(shù)據(jù)結(jié)構(gòu)(即非遞歸結(jié)構(gòu))的類。在Python中正確使用這二者不會(huì)造成循環(huán)引用。因此,recordclass生成的類實(shí)例默認(rèn)情況下不包含PyGC_Head片段(這個(gè)片段是支持循環(huán)垃圾回收機(jī)制的必需字段,或者更準(zhǔn)確地說,在創(chuàng)建類的PyTypeObject結(jié)構(gòu)中,flags字段默認(rèn)情況下不會(huì)設(shè)置Py_TPFLAGS_HAVE_GC標(biāo)志)。
大量實(shí)例占用的內(nèi)存量要小于帶有__slots__的類實(shí)例:
實(shí)例數(shù)
大小
1 000 000
48 Mb
10 000 000
480 Mb
100 000 000
4.8 Gb
dataobject
recordclass庫提出的另一個(gè)解決方案的基本想法為:內(nèi)存結(jié)構(gòu)采用與帶__slots__的類實(shí)例同樣的結(jié)構(gòu),但不參與循環(huán)垃圾回收機(jī)制。這種類可以通過recordclass.make_dataclass函數(shù)生成:
>>>?Point?=?make_dataclass('Point',?('x',?'y',?'z'))
這種方式創(chuàng)建的類默認(rèn)會(huì)生成可修改的實(shí)例。
另一種方法是從recordclass.dataobject繼承:
class?Point(dataobject):
x:int
y:int
z:int
這種方法創(chuàng)建的類實(shí)例不會(huì)參與循環(huán)垃圾回收機(jī)制。內(nèi)存中實(shí)例的結(jié)構(gòu)與帶有__slots__的類相同,但沒有PyGC_Head:
字段
大小(字節(jié))
PyObject_HEAD
16
ob_size
8
x
8
y
8
z
8
總計(jì):
48
>>>?ob?=?Point(1,2,3)
>>>?print(sys.getsizeof(ob))
40
如果想訪問字段,則需要使用特殊的描述符來表示從對(duì)象開頭算起的偏移量,其位置位于類字典內(nèi):
mappingproxy({'__new__':?,
.......................................
'x':?,
'y':?,
'z':?})
大量實(shí)例占用的內(nèi)存量在CPython實(shí)現(xiàn)中是最小的:
實(shí)例數(shù)
大小
1 000 000
40 Mb
10 000 000
400 Mb
100 000 000
4.0 Gb
Cython
還有一個(gè)基于Cython(https://cython.org/)的方案。該方案的優(yōu)點(diǎn)是字段可以使用C語言的原子類型。訪問字段的描述符可以通過純Python創(chuàng)建。例如:
cdef?class?Python:
cdef?public?int?x,?y,?z
def?__init__(self,?x,?y,?z):
self.x?=?x
self.y?=?y
self.z?=?z
本例中實(shí)例占用的內(nèi)存更小:
>>>?ob?=?Point(1,2,3)
>>>?print(sys.getsizeof(ob))
32
內(nèi)存結(jié)構(gòu)如下:
字段
大小(字節(jié))
PyObject_HEAD
16
x
4
y
4
z
4
nycto
4
總計(jì):
32
大量副本所占用的內(nèi)存量也很小:
實(shí)例數(shù)
大小
1 000 000
32 Mb
10 000 000
320 Mb
100 000 000
3.2 Gb
但是,需要記住在從Python代碼訪問時(shí),每次訪問都會(huì)引發(fā)int類型和Python對(duì)象之間的轉(zhuǎn)換。
Numpy
使用擁有大量數(shù)據(jù)的多維數(shù)組或記錄數(shù)組會(huì)占用大量?jī)?nèi)存。但是,為了有效地利用純Python處理數(shù)據(jù),你應(yīng)該使用Numpy包提供的函數(shù)。
>>>?Point?=?numpy.dtype(('x',?numpy.int32),?('y',?numpy.int32),?('z',?numpy.int32)])
一個(gè)擁有N個(gè)元素、初始化成零的數(shù)組可以通過下面的函數(shù)創(chuàng)建:
>>>points?=?numpy.zeros(N,?dtype=Point)
內(nèi)存占用是最小的:
實(shí)例數(shù)
大小
1 000 000
12 Mb
10 000 000
120 Mb
100 000 000
1.2 Gb
一般情況下,訪問數(shù)組元素和行會(huì)引發(fā)Python對(duì)象與C語言int值之間的轉(zhuǎn)換。如果從生成的數(shù)組中獲取一行結(jié)果,其中包含一個(gè)元素,其內(nèi)存就沒那么緊湊了:
>>>?sys.getsizeof(points[0])
68
因此,如上所述,在Pytho代碼中需要使用numpy包提供的函數(shù)來處理數(shù)組。
總結(jié)
在本文中,我們通過一個(gè)簡(jiǎn)單明了的例子,求證了Python語言(CPython)社區(qū)的開發(fā)人員和用戶可以真正減少對(duì)象占用的內(nèi)存量。
原文:https://habr.com/en/post/458518
本文為 CSDN 翻譯,【Python貓】授權(quán)轉(zhuǎn)載。
文章分享完了,最后是隨機(jī)薦書環(huán)節(jié)。我會(huì)根據(jù)文章內(nèi)容,提出一個(gè)關(guān)鍵詞(這篇是“numpy”)來搜索,隨機(jī)選擇一本技術(shù)書推薦給大家,如果你感興趣的話,可以點(diǎn)擊鏈接進(jìn)行了解。希望能給大家?guī)聿唤?jīng)意的收獲~~
告訴朋友們,我在看
總結(jié)
以上是生活随笔為你收集整理的python减少内存_如何降低 Python 的内存消耗量?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python二叉树遍历算法_分享pyth
- 下一篇: lisp坐标一键生成_联排建筑一键生成?