【為什麼我們要挑選這篇文章】記憶體空間不足是工程師很容易遇到的問題,除了關閉不必要的頁面之外,本文作者透過簡單的程式碼,節省了 50% 以上的記憶體空間。(責任編輯:郭家宏)
作者:大數據文摘
編譯:Javen、胡笳、雲舟
記憶體空間不足是專案開發過程中經常碰到的問題,我和我的團隊在之前的一個專案中也遇到了這個問題,我們的專案需要儲存和處理一個相當大的模板,測試人員經常向我抱怨記憶體不足。但是最終,我們通過添加一行簡單的程式碼解決了這個問題。
結果如圖所示:
我將在下面解釋它的工作原理。
舉一個簡單的「learning」示例
創建一個 DataItem 類,在其中定義一些個資屬性,例如姓名,年齡和地址。
class DataItem(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 類和「小測試」問題:
這個類到底占多少記憶體?
首先,我們將以較低級別輸出該類的全部內容:
def dump(obj):
for attr in dir(obj):
print(” obj.%s = %r” % (attr, getattr(obj, attr)))
這個函數將顯示隱藏在「隱身衣」下的內容,以便所有 Python 函數(類型,繼承和其他包)都可以運行。
結果令人印象深刻:
它總共占用多少記憶體呢?
在 GitHub 上,有一個函數可以計算實際大小,透過回歸調用所有對象的 getsizeof 實現。
def get_size(obj, seen=None):
# From https://goshippo.com/blog/measure-real-size-any-python-object/
# Recursively finds size of objects
size = sys.getsizeof(obj)
if seen is None:
seen = set()
obj_id = id(obj)
if obj_id in seen:
return 0# Important mark as seen *before* entering recursion to gracefully handle
# self-referential objects
seen.add(obj_id)
if isinstance(obj, dict):
size += sum([get_size(v, seen) for v in obj.values()])
size += sum([get_size(k, seen) for k in obj.keys()])
elif hasattr(obj, ‘__dict__’):
size += get_size(obj.__dict__, seen)
elif hasattr(obj, ‘__iter__’) and not isinstance(obj, (str, bytes, bytearray)):
size += sum([get_size(i, seen) for i in obj])
return size
讓我們試一下:
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 是一個直譯器(interpreter),我們可以隨時擴展我們的類,例如,添加一個新欄位:
d1 = DataItem(“Alex”, 42, “-“)
print (“get_size(d1):”, get_size(d1))d1.weight = 66
print (“get_size(d1):”, get_size(d1))
這是一個很棒的特點,但是如果我們不需要這個功能,我們可以強制直譯器使用 __slots__ 指令來指定類屬性列表:
class DataItem(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 = []
for p in range(100000):
data.append(DataItem(“Alex”, 42, “middle of nowhere”))snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics(‘lineno’)
total = sum(stat.size for stat in top_stats)
print(“Total allocated size: %.1f MB” % (total / (1024*1024)))
在沒有 __slots__ 的情況結果為 16.8 MB ,而使用 __slots__ 時為 6.9 MB 。當然不是 7 倍,但考慮到代碼變化很小,它的表現依然出色。
現在討論一下這種方式的缺點。啟用 __slots__ 會禁止創建其他所有元素,包括 __dict__ ,這意味著,例如,下面這種將結構轉換為 json 的代碼將不起作用:
def toJSON(self):
return json.dumps(self.__dict__)
但這也很容易搞定,可以通過編程方式生成你的 dict ,遍歷循環中的所有元素:
def toJSON(self):
data = dict()
for var in self.__slots__:
data[var] = getattr(self, var)
return json.dumps(data)
向類中動態添加新變數也是不可能的,但在我們的項目裡,這不是必需的。
下面是最後一個小測試。來看看整個程式需要多少記憶體。在程式末尾添加一個無限循環,使其持續運行,並查看 Windows 任務管理器中的記憶體消耗。
沒有 __slots__ 時:
69 Mb 變成 27 Mb 。好吧,畢竟我們節省了記憶體。對於只添加一行程式碼的結果來說已經很好了。
注意: tracemalloc 調試庫使用了大量額外的記憶體。顯然,它為每個創建的對象添加了額外的元素。如果你將其關閉,總記憶體消耗將會少得多,截圖顯示了 2 個選項:
如何節省更多的記憶體空間?
可以使用 numpy 庫,它允許你以 C 風格創建結構,但在這個的項目中,它需要更深入地改進程式碼,所以對我來說第一種方法就足夠了。
奇怪的是, __slots__ 的使用從未在 Habré 上詳細分析過,我希望這篇文章能夠填補這一空白。
這篇文章看起來似乎是反 Python 的廣告,但它根本不是。 Python 是非常可靠的(為了「刪除」 Python 中的程式碼,你必須非常努力),這是一種易於閲讀和方便編寫的語言。在許多情況下,這些優點遠勝過缺點,但如果你需要性能和效率的最大化,你可以使用 numpy 庫像 C++ 一樣編寫代碼,它可以非常快速有效地處理數據。
最後,祝你 coding 愉快!
關注公眾號:大數據文摘
IG : BigDataDigest
(本文經 大數據文摘 授權轉載,並同意 TechOrange 編寫導讀與修訂標題,原文標題為 〈 没有什么内存问题,是一行Python代码解决不了的〉 。首圖來源:Wikimedia Commons)
更多 Python 技術
【年末轉行指南】無經驗也能做數據科學家?其實學會 Python 後什麼都好談