沒有什麼記憶體問題,是一行Python程式碼解決不了的
記憶體不足是專案開發過程中經常碰到的問題,我和我的團隊在之前的一個專案中也遇到了這個問題,我們的專案需要儲存和處理一個相當大的動態列表,測試人員經常向我抱怨記憶體不足。但是最終,我們通過新增一行簡單的程式碼解決了這個問題。
結果如圖所示:
我將在下面解釋它的工作原理。
舉一個簡單的“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/">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是一個直譯器,我們可以隨時擴充套件我們的類,例如,新增一個新欄位:
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.8MB,而使用__slots__時為6.9MB。當然不是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__時
69Mb變成27Mb......好吧,畢竟我們節省了記憶體。對於只新增一行程式碼的結果來說已經很好了。
注意:tracemalloc除錯庫使用了大量額外的記憶體。顯然,它為每個建立的物件添加了額外的元素。如果你將其關閉,總記憶體消耗將會少得多,截圖顯示了2個選項:
如何節省更多的記憶體?
可以使用numpy庫,它允許你以C風格建立結構,但在這個的專案中,它需要更深入地改進程式碼,所以對我來說第一種方法就足夠了。
奇怪的是,__slots__的使用從未在Habré上詳細分析過,我希望這篇文章能夠填補這一空白。
結論
這篇文章看起來似乎是反Python的廣告,但它根本不是。Python是非常可靠的(為了“刪除”Python中的程式,你必須非常努力),這是一種易於閱讀和方便編寫的語言。在許多情況下,這些優點遠勝過缺點,但如果你需要效能和效率的最大化,你可以使用numpy庫像C++一樣編寫程式碼,它可以非常快速有效地處理資料。
最後,祝你程式設計愉快!
相關報道: ofollow,noindex" target="_blank">https://medium.com/@alexmaisiura/python-how-to-reduce-memory-consumption-by-half-by-adding-just-one-line-of-code-56be6443d524