Python必不可少的小技巧,一行程式碼減少一半記憶體佔用!
我想與大家分享一些我和我的團隊在一個專案中經歷的一些問題。在這個專案中,我們必須要儲存和處理一個相當大的動態列表。測試人員在測試過程中,抱怨記憶體不足。下面介紹一個簡單的方法,通過新增一行程式碼來解決這個問題。
圖片的結果
下面我來解釋一下,它是如何執行的。
首先,我們考慮一個簡單的"learning"例子,建立一個Dataltem 類,該類是一個人的個人資訊,例如姓名,年齡,地址等。
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))
我們得到的答案是56bytes,這似乎佔用了很少的記憶體,相當滿意嘍。那麼,我們在嘗試另一個包含更多資料的物件例子:
d2 = DataItem("Boris", 24, "In the middle of nowhere") print ("sys.getsizeof(d2):", sys.getsizeof(d2))
答案仍然是56bytes,此刻,似乎我們意識到哪裡有些不對?並不是所有的事情都第一眼所見那樣。
- 直覺不會讓我們失望,一切都不是那麼簡單。Python是一種具有動態型別的非常靈活的語言,對於它的工作,它儲存了大量的附加資料。它們本身佔據了很多。
例如,sys.getsizeof("")返回33bytes,是的一個多達33個位元組的空行!並且sys.getsizeof(1)返回24bytes,一個整個數字佔用24個bytes(我想諮詢C語言程式設計師,遠離螢幕,不想在進一步閱讀,以免對美觀失去信心)。對於更復雜的元素,如字典,sys.getsizeof(.())返回272位元組,這是針對空字典的,我不會再繼續了,我希望原理是明確的,並且RAM的製造商需要出售他們的晶片。
但是,我們回到我們的DataItem類和最初的初學者的疑惑。
這個類,佔多少記憶體?
首先,我們一小寫的形式將這個類的完整內容輸出:
def dump(obj):
for attr in dir(obj): print(" obj.%s = %r" % (attr, getattr(obj, attr)))
這個函式將顯示隱藏的“幕後”使所有Python函式(型別、繼承和其他內容)都能夠正常工作的內容。
結果令人印象深刻:
這一切內容佔用多少記憶體?
下邊有一個函式可以通過遞迴的方式,呼叫getsizeof函式,計算物件實際資料量。
def get_size(obj, seen=None): # From # 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))
我們獲得的答案分別為460bytes和484bytes,這結果似乎是真實的。
使用這個函式,你可以進行一系列的實驗。例如,我想知道如果DataItem結構放在列表中,資料將佔用多少空間。get_size ([d1])函式返回532bytes,顯然,這與上面說的460+的開銷相同。但是get_size ([d1, d2])返回863bytes,小於以上的460 + 484。get_size ([d1, d2, d1])的結果更有趣——我們得到了871位元組,只是稍微多一點,也就是說Python足夠聰明,不會再次為同一個物件分配記憶體。
現在,我們來看一看問題的第二部分。
是否存在減少記憶體開銷的可能呢?
是的,可以的。Python是一個直譯器,我們可以在任何時候擴充套件我們的類,例如,新增一個新的欄位:
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
更多資訊可以在文件(RTFM)中找到,其中寫到“__ 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.8MB,使用時佔用6.9MB。這個操作當然不是最好的,但是確實程式碼改變的最小的。(Not 7 times of course, but it’s not bad at all, considering that the code change was minimal.)
現在的缺點。啟用__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__:
6.9Mb 變成 27Mb … 好傢伙, 畢竟, 我們節省了記憶體, 27Mb 代替 70 ,對於增加一行程式碼來說並不是一個壞的例子
注意:TraceMelc除錯庫使用了許多附加記憶體。顯然,她為每個建立的物件添加了額外的元素。如果關閉它,總的記憶體消耗將少得多,截圖顯示兩個選項:
如果你想節省更多的記憶體呢?
這可以使用numpy庫,它允許您以C樣式建立結構,但是在我的例子中,它需要對程式碼進行更深入的細化,並且第一種方法就足夠了。
奇怪的是在Habré從來沒有詳細分析使用__slots__,我希望本文將填補這一空缺。
結論
這篇文章似乎是一個anti-Python廣告,但並不是。Python非常可靠(為了“降低”Python程式,您必須非常努力),它是一種易於閱讀和方便編寫程式碼的語言。這些優點在很多情況下都大於缺點,但是如果您需要最大的效能和效率,您可以使用像numpy這樣的庫,它是用C++編寫的,它可以很快和高效地與資料一起工作。