當前位置: 妍妍網 > 辦公

7個Python記憶體最佳化技巧,你用過幾個?

2024-03-11辦公

當我們的計畫變得越來越大時,高效管理計算資源是一個不可避免的要求。 不幸的是,與低階語言如C或C++相比,Python在記憶體效率方面似乎不夠。 那麽,現在應該更改程式語言嗎?

當然不是。 事實上,有許多方法可以顯著最佳化Python程式的記憶體使用,從優秀的模組和工具到先進的數據結構和演算法。 本文將聚焦於Python的內建機制,並介紹7個原始但有效的記憶體最佳化技巧。 掌握這些技巧將顯著提高我們的Python編程技能。

1. 在類別定義中使用__slots__

Python作為一種動態型別語言,在物件導向編程方面更加靈活。一個很好的例子是在執行時向Python類中添加額外的內容和方法的能力。 例如,下面的程式碼定義了一個名為Author的類。 最初它有兩個內容name和age。 但是我們可以很容易地在後來添加一個額外的內容:

classAuthor:def__init__(self, name, age):self.name = nameself.age = ageme = Author('Yang Zhou', 30)me.job = 'Software Engineer'print(me.job)

然而,每個硬幣都有兩面。這種靈活性在底層浪費了更多的記憶體。 因為Python 類的每個例項都維護一個特殊的字典(__dict__)來儲存例項變量。 這個字典由於其基於哈希表的實作方式而固有地記憶體效率低下,占用大量記憶體。

在大多數情況下,我們不需要在執行時更改例項的變量或方法,而且在類別定義之後__dict__將不會改變。因此,如果我們能避免維護__dict__字典,那就更好了。 Python為此提供了一個神奇的內容: __slots__。 它透過指定類的所有有效內容的名稱來充當白名單:

classAuthor: __slots__ = ('name', 'age')def__init__(self, name, age):self.name = nameself.age = ageme = Author('Yang Zhou', 30)me.job = 'Software Engineer'print(me.job)#AttributeError: 'Author' object has no attribute 'job'

如上所示,我們不能再在執行時添加job內容。因為__slots__白名單只定義了兩個有效內容name和age。 從理論上講,由於內容現在是固定的,Python不需要為其維護一個字典。 它只需為__slots__中定義的內容分配必要的記憶體空間。

讓我們編寫一個簡單的比較程式,看看它是否確實起作用:

import sys classAuthor:def__init__(self, name, age):self.name = nameself.age = age classAuthorWithSlots: __slots__ = ['name', 'age']def__init__(self, name, age):self.name = nameself.age = age# Creating instancesme = Author('Yang', 30)me_with_slots = AuthorWithSlots('Yang', 30)# Comparing memory usagememory_without_slots = sys.getsizeof(me) + sys.getsizeof(me.__dict__)memory_with_slots = sys.getsizeof(me_with_slots) # __slots__ classes don't have __dict__print(memory_without_slots, memory_with_slots)# 152 48print(me.__dict__)# {'name': 'Yang', 'age': 30}print(me_with_slots.__dict__)# AttributeError: 'AuthorWithSlots' object has no attribute '__dict__'

正如上面的程式碼所演示的,由於使用了__slots__,me_with_slots例項不具有__dict__字典。與必須保留額外字典的me例項相比,這有效地節省了記憶體資源。

2. 使用生成器

生成器是Python中的惰性求值版本的列表。 它們就像元素生成工廠: 僅在呼叫next()方法時生成一個計畫,而不是一次計算所有計畫。 因此,當處理大型數據集時,它們非常記憶體高效。

defnumber_generator():for i in range(100):yield inumbers = number_generator()print(numbers)print(next(numbers))#0print(next(numbers))#1

上面的程式碼展示了編寫和使用生成器的基本範例。關鍵字yield是生成器定義的核心。套用它意味著只有在呼叫next()方法時才會產生計畫i。 現在,讓我們比較一下生成器和列表,看看哪個更記憶體高效:

import sysnumbers = []for i in range(100): numbers.append(i)defnumber_generator():for i in range(100):yield inumbers_generator = number_generator()print(sys.getsizeof(numbers_generator))#112print(sys.getsizeof(numbers))#920

上述程式的結果證明了使用生成器可以顯著節省記憶體使用。 順便說一下,如果我們將列表推導式的方括弧改成括弧,它將變成生成器運算式。 這是在Python中定義生成器的更簡便的方法:

import sysnumbers = [i for i in range(100)]numbers_generator = (i for i in range(100))print(sys.getsizeof(numbers_generator))#112print(sys.getsizeof(numbers))#920

3. 利用記憶體對映檔支持大檔處理

記憶體對映檔I/O,簡稱「mmap」,是一種作業系統級別的最佳化。

它實作了需求分頁,因為檔內容並不立即從磁盤讀取,並且最初根本不使用物理RAM。實際從磁盤讀取是在特定位置被存取時以懶惰的方式執行的。—— 維基百科

簡單來說,當使 用mmap技術記憶體對映檔時,它在當前行程的虛擬記憶體空間中直接建立檔的對映,而不是將整個檔載入到記憶體中。 對映而不是載入整個檔可以節省大量記憶體。

聽起來很復雜? 幸運的是,Python已經提供了一個用於使用這種技術的內建模組,因此我們可以輕松利用它,而不必考慮作業系統級別的實作。 例如,這是在Python中使用mmap進行檔處理的方法:

import mmapwith open('test.txt', "r+b") as f:# memory-map the file, size 0 means whole filewith mmap.mmap(f.fileno(), 0) as mm:# read content via standard file methods print(mm.read())# read content via slice notation snippet = mm[0:10] print(snippet.decode('utf-8'))

如上所演示的,Python使得記憶體對映檔I/O技術的使用變得方便。我們所需要做的就是簡單地套用`mmap.mmap()`方法,然後使用標準檔方法或甚至切片表示法處理開啟的物件。

4. 減少全域變量的使用

全域變量在程式執行期間始終駐留在記憶體中,因為它們具有全域範圍。 因此,如果一個全域變量保存一個大型數據結構,它將在整個程式生命周期中占用記憶體,可能導致記憶體使用效率低下。 我們應該在Python程式碼中盡量減少全域變量的使用。

5. 利用邏輯運算子的短路求值

這個技巧似乎微妙,但巧妙地使用它將極大地節省程式的記憶體使用。 例如,下面是一個簡單的程式碼片段,根據兩個函式返回的布爾值得到最終結果:

result_a = expensive_function_a()result_b = expensive_function_b()result = result_a if result_a else result_b

上面的程式碼能夠工作,但實際上執行了兩個記憶體效率低下的函式。 獲取相同結果的更聰明的方法如下:

result = expensive_function1() orexpensive_function2()

由於邏輯運算子遵循短路求值規則,上述程式碼中的`expensive_function2()`將不會在`expensive_function1()`為True時執行。這將節省不必要的記憶體使用。

6. 謹慎選擇數據型別

一位經驗豐富的Python開發者會仔細而準確地選擇數據型別。 因為在某些場景中,使用一個數據型別比另一個更節省記憶體。

元組比列表更節省記憶體

由於元組是不可變的(在建立後不能更改),它允許Python在記憶體分配方面進行最佳化。 然而,列表是可變的,因此需要額外的空間來容納潛在的修改。

import sysmy_tuple = (1, 2, 3, 4, 5)my_list = [1, 2, 3, 4, 5]print(sys.getsizeof(my_tuple))#80print(sys.getsizeof(my_list))#120

如上面的片段所示,即使它們包含相同的元素,元組`my_tuple`使用的記憶體比列表更少。 因此,如果在建立後不需要更改數據,我們應該更喜歡使用元組而不是列表。

陣列比列表更節省記憶體

Python中的陣列要求元素是相同的數據型別(例如,全部整數或全部浮點數),但列表可以儲存不同型別的物件,這必然需要更多的記憶體。 因此,如果列表的元素都是相同型別,使用陣列會更節省記憶體:

import sysimport arraymy_list = [i for i in range(1000)]my_array = array.array('i', [i for i in range(1000)])print(sys.getsizeof(my_list))#8856print(sys.getsizeof(my_array))#4064

優秀的數據科學模組比內建數據型別更高效

Python是數據科學的主導語言。有許多強大的第三方模組和工具提供了更多的數據型別,例如NumPy和Pandas。 如果我們只需要一個簡單的一維數位陣列,並且不需要NumPy提供的廣泛功能,那麽Python內建的陣列可能是一個不錯的選擇。

但是,當涉及到復雜的矩陣操作時,對於所有數據科學家來說,使用NumPy提供的陣列是第一選擇,可能是最好的選擇。

7. 對相同的字串套用字串駐留技術

下面的程式碼可能會使許多開發者感到困惑:

>>> a = 'Y'*4096>>> b = 'Y'*4096>>> a is bTrue>>> c = 'Y'*4097>>> d = 'Y'*4097>>> c is dFalse

正如我們所知,`is`運算子用於檢查兩個變量是否參照記憶體中的同一物件。它與`==`運算子不同,後者用於比較兩個物件是否具有相同的值。 那麽為什麽`a is b`返回True,而`c is d`返回False呢?

這裏有Python中的一個隱秘技巧 —— 字串駐留技術。 如果有幾個值相同的小型字串,它們將由Python隱式地進行駐留,並參照記憶體中的同一物件。 定義小型字串的神奇數位是4096。 由於`c`和`d`的長度都是4097,它們是記憶體中的兩個物件而不是一個。 不再有隱式的字串駐留。 因此,在執行`c is d`時得到False。

字串駐留是一種最佳化記憶體使用的強大技術。如果我們想要顯式地進行駐留,`sys.intern()`方法就派上用場了:

>>> import sys>>> c = sys.intern('Y'*4097)>>> d = sys.intern('Y'*4097)>>> c is dTrue

順便說一下,除了字串駐留,Python還對小整數套用駐留技巧。我們也可以利用它進行記憶體最佳化。

來源:小白玩轉Python

作者:二旺

Crossin的新書【 碼上行動:用ChatGPT學會Python編程 】已經上市了。 本書以ChatGPT為輔助,系統全面地講解了如何掌握Python編程,適合Python零基礎入門的讀者學習。

購買後可加入讀者交流群,Crossin為你開啟陪讀模式,解答你在閱讀本書時的一切疑問。

Crossin的其他書籍:

添加微信 crossin123 ,加入編程教室共同學習 ~

感謝 轉發 點贊 的各位~