沒有什麼內存問題,是一行Python代碼解決不了的

尋夢新聞LINE@每日推播熱門推薦文章,趣聞不漏接❤️

加入LINE好友

沒有什麼內存問題,是一行Python代碼解決不了的 科技 第1張

大數據文摘出品

編譯:Javen、胡笳、雲舟

內存不足是項目開發過程中經常碰到的問題,我和我的團隊在之前的一個項目中也遇到了這個問題,我們的項目需要存儲和處理一個相當大的動態列表,測試人員經常向我抱怨內存不足。但是最終,我們通過添加一行簡單的代碼解決了這個問題。

結果如圖所示:

沒有什麼內存問題,是一行Python代碼解決不了的 科技 第2張

我將在下面解釋它的工作原理。

舉一個簡單的「learning」示例 – 創建一個DataItem類,在其中定義一些個人信息屬性,例如姓名,年齡和地址。

classDataItem(object): def__init__(self, name, age, address): self.name = name self.age = age self.address = address

小測試——這樣一個對象會占用多少內存?

首先讓我們嘗試下面這種測試方案:

d1 = DataItem("Alex",42,"-")print("sys.getsizeof(d1):", sys.getsizeof(d1))

答案是56字節。看起來比較小,結果令人滿意。

但是,讓我們檢查另一個數據多一些的對象:

d2 = DataItem("Boris",24,"In the middle of nowhere")print("sys.getsizeof(d2):", sys.getsizeof(d2))

答案仍然是56。這讓我們明白這個結果並不完全正確。

我們的直覺是對的,這個問題不是那麼簡單。Python是一種非常靈活的語言,具有動態類型,它在工作時存儲了許多額外的數據。這些額外的數據本身就占了很多內存。

例如,sys.getsizeof(「 」)返回33,沒錯,每個空行就多達33字節!並且sys.getsizeof(1)將為此數字返回24-24個字節(我建議C工程師們現在點擊結束閱讀,以免對Python的美麗失去信心)。

對於更複雜的元素,例如字典,sys.getsizeof(dict())返回272個字節,這還只是一個空字典。舉例到此為止,但事實已經很清楚了,何況RAM的製造商也需要出售他們的晶片。

現在,讓我們回到回到我們的DataItem類和「小測試」問題。

這個類到底占多少內存?

首先,我們將以較低級別輸出該類的全部內容:

defdump(obj):forattrindir(obj): print(" obj.%s = %r"% (attr, getattr(obj, attr)))

這個函數將顯示隱藏在「隱身衣」下的內容,以便所有Python函數(類型,繼承和其他包)都可以運行。

結果令人印象深刻:

沒有什麼內存問題,是一行Python代碼解決不了的 科技 第3張

它總共占用多少內存呢?

在GitHub上,有一個函數可以計算實際大小,通過遞歸調用所有對象的getsizeof做到。

defget_size(obj, seen=None): # From https://goshippo.com/blog/measure-real-size-any-python-object/ # Recursively finds size of objects size = sys.getsizeof(obj) ifseenisNone: seen = set() obj_id = id(obj) ifobj_idinseen: return0# Important mark as seen *before* entering recursion to gracefully handle # self-referential objects seen.add(obj_id) ifisinstance(obj, dict): size += sum([get_size(v, seen)forvinobj.values()]) size += sum([get_size(k, seen)forkinobj.keys()]) elifhasattr(obj,'__dict__'): size += get_size(obj.__dict__, seen) elifhasattr(obj,'__iter__')andnotisinstance(obj, (str, bytes, bytearray)): size += sum([get_size(i, seen)foriinobj]) returnsize

讓我們試一下:

d1 = DataItem("Alex",42,"-")print("get_size(d1):", get_size(d1))d2 = DataItem("Boris",24,"In the middle of nowhere")print("get_size(d2):", get_size(d2))

我們分別得到460和484字節,這似乎更接近事實。

使用這個函數,我們可以進行一系列實驗。例如,我想知道如果DataItem放在列表中,數據將占用多少空間。

get_size([d1])函數返回532個字節,顯然,這些是「原本的」460+一些額外開銷。但是get_size([d1,d2])返回863個字節—小於460+484。get_size([d1,d2,d1])的結果更加有趣,它產生了871個字節,只是稍微多了一點,這說明Python很聰明,不會再為同一個對象分配內存。

現在我們來看問題的第二部分。

是否有可能減少內存消耗?

答案是肯定的。Python是一個解釋器,我們可以隨時擴展我們的類,例如,添加一個新字段:

d1 = DataItem("Alex",42,"-")print("get_size(d1):", get_size(d1))d1.weight =66print("get_size(d1):", get_size(d1))

這是一個很棒的特點,但是如果我們不需要這個功能,我們可以強制解釋器使用__slots__指令來指定類屬性列表:

classDataItem(object): __slots__ = ['name','age','address'] def__init__(self, name, age, address): self.name = name self.age = age self.address = address

更多信息可以參考文檔中的「__dict__和__weakref__的部分。使用__dict__所節省的空間可能會很大」。

我們嘗試後發現:get_size(d1)返回的是64字節,對比460直接,減少約7倍。作為獎勵,對象的創建速度提高了約20%(請參閱文章的第一個螢幕截圖)。

真正使用如此大的內存增益不會導致其他開銷成本。只需添加元素即可創建100,000個數組,並查看內存消耗:

data = []forpinrange(100000): data.append(DataItem("Alex",42,"middle of nowhere"))snapshot = tracemalloc.take_snapshot()top_stats = snapshot.statistics('lineno')total = sum(stat.sizeforstatintop_stats)print("Total allocated size: %.1f MB"% (total / (1024*1024)))

在沒有__slots__的情況結果為16.8MB,而使用__slots__時為6.9MB。當然不是7倍,但考慮到代碼變化很小,它的表現依然出色。

現在討論一下這種方式的缺點。激活__slots__會禁止創建其他所有元素,包括__dict__,這意味著,例如,下面這種將結構轉換為json的代碼將不起作用:

deftoJSON(self): returnjson.dumps(self.__dict__)

但這也很容易搞定,可以通過編程方式生成你的dict,遍歷循環中的所有元素:

deftoJSON(self): data = dict() forvarinself.__slots__: data[var] = getattr(self, var) returnjson.dumps(data)

向類中動態添加新變量也是不可能的,但在我們的項目里,這不是必需的。

下面是最後一個小測試。來看看整個程序需要多少內存。在程序末尾添加一個無限循環,使其持續運行,並查看Windows任務管理器中的內存消耗。

沒有__slots__時

沒有什麼內存問題,是一行Python代碼解決不了的 科技 第4張

69Mb變成27Mb……好吧,畢竟我們節省了內存。對於只添加一行代碼的結果來說已經很好了。

注意:tracemalloc調試庫使用了大量額外的內存。顯然,它為每個創建的對象添加了額外的元素。如果你將其關閉,總內存消耗將會少得多,截圖顯示了2個選項:

沒有什麼內存問題,是一行Python代碼解決不了的 科技 第5張

如何節省更多的內存?

可以使用numpy庫,它允許你以C風格創建結構,但在這個的項目中,它需要更深入地改進代碼,所以對我來說第一種方法就足夠了。

奇怪的是,__slots__的使用從未在Habré上詳細分析過,我希望這篇文章能夠填補這一空白。

結論

這篇文章看起來似乎是反Python的廣告,但它根本不是。Python是非常可靠的(為了「刪除」Python中的程序,你必須非常努力),這是一種易於閱讀和方便編寫的語言。在許多情況下,這些優點遠勝過缺點,但如果你需要性能和效率的最大化,你可以使用numpy庫像C++一樣編寫代碼,它可以非常快速有效地處理數據。

最後,祝你編程愉快!

相關報導:

https://medium.com/@alexmaisiura/python-how-to-reduce-memory-consumption-by-half-by-adding-just-one-line-of-code-56be6443d524

【今日機器學習概念】

Have a Great Definition

沒有什麼內存問題,是一行Python代碼解決不了的 科技 第6張

沒有什麼內存問題,是一行Python代碼解決不了的 科技 第7張

志願者介紹

後台回復「志願者」加入我們

沒有什麼內存問題,是一行Python代碼解決不了的 科技 第8張

沒有什麼內存問題,是一行Python代碼解決不了的 科技 第9張

沒有什麼內存問題,是一行Python代碼解決不了的 科技 第10張

沒有什麼內存問題,是一行Python代碼解決不了的 科技 第11張

沒有什麼內存問題,是一行Python代碼解決不了的 科技 第12張

About 尋夢園
尋夢園是台灣最大的聊天室及交友社群網站。 致力於發展能夠讓會員們彼此互動、盡情分享自我的平台。 擁有數百間不同的聊天室 ,讓您隨時隨地都能找到志同道合的好友!