在程序執行期間,如果內存中存在大量處于活動狀態的對象,就有可能出現內存問題,尤其是在可用內存總量有限的情況下。在本文中,我們將討論通過縮小對象大幅減少Python所需內存量的方法。
作者 |? intellimath
譯者 |?彎月,責編 | 郭芮
出品 | CSDN(ID:CSDNnews)
以下為譯文:
為了簡便起見,我們以一個表示點的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占用了大量內存,尤其是如果突然虛需要創建大量實例時:
實例數 |
對象大小 |
1 000 000 |
240 Mb |
10 000 000 |
2.40 Gb |
100 000 000 |
24 Gb |
?
類實例
?
有些人希望將所有東西都封裝到類中,他們更喜歡將結構定義為可以通過屬性名訪問的類:
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 |
不難看出,由于實例的字典很大,所以實例依然占用了大量內存。
?
帶有__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__都可以利用這個包自動創建類。
?
元組
?
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 |
?
命名元組
?
由于元組的使用非常廣泛,所以終有一天你需要通過名稱訪問元組。為了滿足這種需求,你可以使用模塊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 |
?
記錄類:不帶循環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 |
?
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 |
?
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對象之間的轉換。
?
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包提供的函數來處理數組。
?
總結
?
在本文中,我們通過一個簡單明了的例子,求證了Python語言(CPython)社區的開發人員和用戶可以真正減少對象占用的內存量。
原文:https://habr.com/en/post/458518/
本文為 CSDN 翻譯,轉載請注明來源出處。
學Python想要達到大牛高度,你得這么學!
https://edu.csdn.net/topic/python115?utm_source=csdn_bw
【END】
?熱 文 ?推 薦?
? “5 年內,PC 或將逐漸消失!”| 人物志
? 直接拿來用!GitHub 標星 5000+,學生黨學編程有這份資料就夠了
? 華人學者解開計算機領域 30 年難題:布爾函數敏感度猜想
?真實揭秘 90 后程序員奔三準備:有人學金融投資,有人想當全棧工程師!
?天網恢恢!又一名暗網比特幣洗錢者被抓了
?乘勢而起,走進2019年風口“邊緣計算”
?Python之父新發文,將替換現有解析器
?超全!深度學習在計算機視覺領域的應用一覽
?中國第一程序員,微軟得不到他就要毀了他!
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
