【工程師葵花寶典】記憶體不夠怎麼辦?一條程式碼,幫你節省 50% 以上的空間 | TechOrange 科技報橘
Search
Close this search box.

【工程師葵花寶典】記憶體不夠怎麼辦?一條程式碼,幫你節省 50% 以上的空間

【為什麼我們要挑選這篇文章】記憶體空間不足是工程師很容易遇到的問題,除了關閉不必要的頁面之外,本文作者透過簡單的程式碼,節省了 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 後什麼都好談

手把手教零售商用 Python 做出採購量預測模型,人力成本通通省下來!

程式語言手刀必存:Python 的數據類該如何理解?