Python內(nèi)存優(yōu)化的七種技巧分享
當(dāng)您的項目規(guī)模越來越大時,高效管理內(nèi)存資源就成為必然要求。遺憾的是,Python,尤其是與 C 或 C++ 等低級語言相比,似乎內(nèi)存效率不夠高?,F(xiàn)在是否應(yīng)該更換編程語言?當(dāng)然不是。事實上,從優(yōu)秀的模塊和工具到先進的數(shù)據(jù)結(jié)構(gòu)和算法,有很多方法可以顯著優(yōu)化 Python 程序的內(nèi)存使用。
本文將重點介紹 Python 的內(nèi)置機制,并介紹 7 種原始但有效的內(nèi)存優(yōu)化技巧。掌握這些技巧將大大提高你的 Python 編程能力。
1. 在類定義中使用 __slots__
Python 作為一種動態(tài)類型編程語言,在 OOP 方面有更大的靈活性。在運行時向 Python 類中添加額外的屬性和方法就是一個很好的例子。
例如,下面的代碼定義了一個名為 Author 的類。它最初有兩個屬性 name 和 age。但我們可以在稍后輕松地添加一個額外的屬性:
class Author: def __init__(self, name, age): self.name = name self.age = age me = Author('Yang Zhou', 30) me.job = 'Software Engineer' print(me.job) # Software Engineer
然而,任何硬幣都有兩面。這種靈活性會浪費更多內(nèi)存。
因為 Python 中每個類的實例都會維護一個特殊的字典 (__dict__
),用于存儲實例變量。由于字典的底層是基于哈希表的實現(xiàn),因此字典本身的內(nèi)存效率很低,因此字典會消耗大量內(nèi)存。
在大多數(shù)情況下,我們不需要在運行時更改實例的變量或方法,而且在類定義之后,__dict__
也不會被更改。因此,我們最好不要維護 __dict__
字典。
Python 為此提供了一個神奇的屬性:__slots__
。
它通過指定類的所有有效屬性的名稱,起到白名單的作用:
class Author: __slots__ = ('name', 'age') def __init__(self, name, age): self.name = name self.age = age me = Author('Yang Zhou', 30) me.job = 'Software Engineer' print(me.job) # AttributeError: 'Author' object has no attribute 'job'
就像上面的代碼,我們不能再在運行時添加工作屬性了。因為 __slots__
白名單只定義了 name
和 age
兩個有效屬性。
從理論上講,由于屬性是固定的,Python 不需要為它維護一個字典。它只需為 __slots__
中定義的屬性分配必要的內(nèi)存空間即可。
讓我們寫一個簡單的比較程序來看看它是否真的可以這樣工作:
import sys class Author: def __init__(self, name, age): self.name = name self.age = age class AuthorWithSlots: __slots__ = ['name', 'age'] def __init__(self, name, age): self.name = name self.age = age # Creating instances me = Author('Yang', 30) me_with_slots = AuthorWithSlots('Yang', 30) # Comparing memory usage memory_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 48 print(me.__dict__) # {'name': 'Yang', 'age': 30} print(me_with_slots.__dict__) # AttributeError: 'AuthorWithSlots' object has no attribute '__dict__'
如上面的代碼所示,由于使用了 __slots__
,實例 me_with_slots
沒有__dict__
字典。與需要保存額外字典的 me
實例相比,它有效地節(jié)省了內(nèi)存資源。
2. 使用生成器
生成器是 Python 中列表的懶版本。它們的工作方式類似于元素生成工廠:每當(dāng)調(diào)用 next()
方法時就生成一個項,而不是一次性計算所有項。因此,在處理大型數(shù)據(jù)集時,它們非常節(jié)省內(nèi)存。
def number_generator(): for i in range(100): yield i numbers = number_generator() print(numbers) # <generator object number_generator at 0x104a57e40> print(next(numbers)) # 0 print(next(numbers)) # 1
上面的代碼展示了一個編寫和使用生成器的基本示例。關(guān)鍵字 yield
是生成器定義的核心。使用它意味著只有在調(diào)用 next()
方法時,才會產(chǎn)生項目 i
。
現(xiàn)在,讓我們比較一下生成器和列表,看看哪一個更節(jié)省內(nèi)存:
import sys numbers = [] for i in range(100): numbers.append(i) def number_generator(): for i in range(100): yield i numbers_generator = number_generator() print(sys.getsizeof(numbers_generator)) # 112 print(sys.getsizeof(numbers)) # 920
上述程序的結(jié)果證明,使用生成器可以大大節(jié)省內(nèi)存使用量。
順便提一下,如果我們把 list
理解的方括號轉(zhuǎn)換成小括號,它就會變成一個生成器表達式。這是在 Python 中定義生成器的一種更簡單的方法:
import sys numbers = [i for i in range(100)] numbers_generator = (i for i in range(100)) print(sys.getsizeof(numbers_generator)) # 112 print(sys.getsizeof(numbers)) # 920
3. 利用內(nèi)存映射文件支持大文件處理
內(nèi)存映射文件 I/O,簡稱 mmap
,是一種操作系統(tǒng)級優(yōu)化。
維基百科:它實現(xiàn)了需求分頁,因為文件內(nèi)容不會立即從磁盤讀取,最初根本不使用物理 RAM。從磁盤實際讀取的操作是在訪問特定位置后,以一種懶惰的方式進行的。
簡單地說,當(dāng)使用 mmap
技術(shù)對文件進行內(nèi)存映射時,它會直接在當(dāng)前進程的虛擬內(nèi)存空間中創(chuàng)建文件的映射,而不是將整個文件加載到內(nèi)存中。映射而不是加載整個文件可以節(jié)省大量內(nèi)存。
看起來很復(fù)雜?幸運的是,Python 已經(jīng)提供了使用這種技術(shù)的內(nèi)置模塊,因此我們可以輕松利用它,而無需考慮操作系統(tǒng)級的實現(xiàn)。
例如,在 Python 中如何使用 mmap
進行文件處理:
import mmap with open('test.txt', "r+b") as f: # memory-map the file, size 0 means whole file with 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 使內(nèi)存映射文件 I/O 技術(shù)的使用變得非常方便。我們需要做的僅僅是應(yīng)用 mmap.mmap()
方法,然后使用標(biāo)準(zhǔn)文件方法甚至切片符號來處理打開的對象。
4. 盡量少用全局變量
全局變量具有全局作用域,因此只要程序運行,全局變量就會一直保留在內(nèi)存中。
因此,如果一個全局變量包含一個大型數(shù)據(jù)結(jié)構(gòu),它就會在整個程序生命周期中占用內(nèi)存,從而可能導(dǎo)致內(nèi)存使用效率低下。
我們應(yīng)該在 Python 代碼中盡量減少全局變量的使用。
5. 利用邏輯操作符
這個技巧看似微妙,但巧妙地使用它將極大地節(jié)省程序的內(nèi)存使用量。
例如,下面是一個簡單的代碼片段,它根據(jù)兩個函數(shù)返回的布爾值得到最終結(jié)果:
result_a = expensive_function_a() result_b = expensive_function_b() result = result_a if result_a else result_b
上述代碼可以正常運行,但它實際上執(zhí)行了兩個內(nèi)存不足的函數(shù)。
獲得相同結(jié)果的更聰明的方法如下:
result = expensive_function1() or expensive_function2()
由于邏輯運算符遵循短路評估規(guī)則,如果 expensive_function1()
為 True
,則不會執(zhí)行上述代碼中的 expensive_function2()
。這將節(jié)省不必要的內(nèi)存使用。
6. 謹(jǐn)慎選擇數(shù)據(jù)類型
資深的 Python 開發(fā)人員會謹(jǐn)慎而精確地選擇數(shù)據(jù)類型。因為在某些情況下,使用一種數(shù)據(jù)類型比使用另一種數(shù)據(jù)類型更節(jié)省內(nèi)存。
元組比列表更節(jié)省內(nèi)存
鑒于元組是不可變的(創(chuàng)建后不能更改),它允許 Python 在內(nèi)存分配方面進行優(yōu)化。然而,列表是可變的,因此需要額外的空間來容納潛在的修改。
import sys my_tuple = (1, 2, 3, 4, 5) my_list = [1, 2, 3, 4, 5] print(sys.getsizeof(my_tuple)) # 80 print(sys.getsizeof(my_list)) # 120
如上面的代碼段所示,即使包含相同的元素,元組 my_tuple
使用的內(nèi)存也比 list
少。
因此,如果在創(chuàng)建后不需要更改數(shù)據(jù),我們應(yīng)該首選元組而不是列表。
數(shù)組比 list 更節(jié)省內(nèi)存
Python 中的數(shù)組要求元素具有相同的數(shù)據(jù)類型(例如,所有整數(shù)或所有浮點數(shù)),但列表可以存儲不同類型的對象,這就不可避免地需要更多內(nèi)存。
因此,如果列表的元素都是同一類型,使用數(shù)組會更節(jié)省內(nèi)存:
import sys import array my_list = [i for i in range(1000)] my_array = array.array('i', [i for i in range(1000)]) print(sys.getsizeof(my_list)) # 8856 print(sys.getsizeof(my_array)) # 4064
優(yōu)秀的數(shù)據(jù)科學(xué)模塊比內(nèi)置數(shù)據(jù)類型更高效
Python 是數(shù)據(jù)科學(xué)的統(tǒng)治語言。有許多強大的第三方模塊和工具提供了更多的數(shù)據(jù)類型,如 NumPy 和 Pandas。
如果我們只需要一個簡單的一維數(shù)組,而不需要 NumPy 提供的廣泛功能,那么 Python 的內(nèi)置數(shù)組可能是一個不錯的選擇。
但如果需要進行復(fù)雜的矩陣操作,使用 NumPy 提供的數(shù)組可能是所有數(shù)據(jù)科學(xué)家的首選,也可能是最佳選擇。
7. 對相同字符串應(yīng)用字符串互文技術(shù)
下面的代碼會讓很多開發(fā)人員感到困惑:
>>> a = 'Y'*4096 >>> b = 'Y'*4096 >>> a is b True >>> c = 'Y'*4097 >>> d = 'Y'*4097 >>> c is d False
我們知道,is
運算符用于檢查兩個變量是否指向內(nèi)存中的同一個對象。它與 ==
運算符不同,后者用于比較兩個對象是否具有相同的值。
那么,為什么 a is b
得到的是 True
,而 c is d
得到的卻是 False
呢?
如果有幾個小字符串的值相同,Python 就會隱式地對它們進行內(nèi)聯(lián),并引用內(nèi)存中的同一個對象。
定義小字符串的神奇數(shù)字是 4096。因為 c 和 d 的長度都是 4097,所以它們在內(nèi)存中是兩個對象,而不是一個。不再有隱式字符串互調(diào)。因此,當(dāng)執(zhí)行 c 是 d 時,我們會得到一個 False。
字符串互調(diào)是一種優(yōu)化內(nèi)存使用的強大技術(shù)。如果我們想顯式地進行字符串互調(diào),sys.intern()
方法就很好用:
>>> import sys >>> c = sys.intern('Y'*4097) >>> d = sys.intern('Y'*4097) >>> c is d True
順便說一下,除了字符串互調(diào),Python 還將互調(diào)技巧應(yīng)用于小整數(shù)。我們還可以利用它來優(yōu)化內(nèi)存。
以上就是Python內(nèi)存優(yōu)化的七種技巧分享的詳細(xì)內(nèi)容,更多關(guān)于Python內(nèi)存優(yōu)化的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
人工智能學(xué)習(xí)Pytorch張量數(shù)據(jù)類型示例詳解
這篇文章主要為大家介紹了人工智能學(xué)習(xí)Pytorch張量數(shù)據(jù)類型的示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步2021-11-11Python Django 添加首頁尾頁上一頁下一頁代碼實例
這篇文章主要介紹了Python Django 添加首頁尾頁上一頁下一頁代碼實例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-08-08python中使用Celery容聯(lián)云異步發(fā)送驗證碼功能
Celery 是一個 基于python開發(fā)的分布式異步消息任務(wù)隊列,通過它可以輕松的實現(xiàn)任務(wù)的異步處理,本文重點給大家介紹使用Celery容聯(lián)云異步發(fā)送驗證碼功能,感興趣的朋友一起看看吧2021-09-09pandas dataframe中雙中括號和單中括號的區(qū)別及說明
這篇文章主要介紹了pandas dataframe中雙中括號和單中括號的區(qū)別及說明,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-08-08python中浮點數(shù)比較判斷!為什么不能用==(推薦)
這篇文章主要介紹了python中浮點數(shù)比較判斷!為什么不能用==,本文給大家分享問題解決方法,需要的朋友可以參考下2023-09-09conda虛擬環(huán)境下使用pyinstaller打包程序為exe文件全過程
pyinstaller是一個支持跨平臺使用的第三方庫,它可以將腳本執(zhí)行所需的模塊和庫,自動分析、收集并生成一個文件夾或者可執(zhí)行文件,這篇文章主要給大家介紹了關(guān)于conda虛擬環(huán)境下使用pyinstaller打包程序為exe文件的相關(guān)資料,需要的朋友可以參考下2023-12-12