Python內(nèi)存優(yōu)化的七種技巧分享
當(dāng)您的項(xiàng)目規(guī)模越來越大時(shí),高效管理內(nèi)存資源就成為必然要求。遺憾的是,Python,尤其是與 C 或 C++ 等低級(jí)語言相比,似乎內(nèi)存效率不夠高?,F(xiàn)在是否應(yīng)該更換編程語言?當(dāng)然不是。事實(shí)上,從優(yōu)秀的模塊和工具到先進(jìn)的數(shù)據(jù)結(jié)構(gòu)和算法,有很多方法可以顯著優(yōu)化 Python 程序的內(nèi)存使用。
本文將重點(diǎn)介紹 Python 的內(nèi)置機(jī)制,并介紹 7 種原始但有效的內(nèi)存優(yōu)化技巧。掌握這些技巧將大大提高你的 Python 編程能力。
1. 在類定義中使用 __slots__
Python 作為一種動(dòng)態(tài)類型編程語言,在 OOP 方面有更大的靈活性。在運(yùn)行時(shí)向 Python 類中添加額外的屬性和方法就是一個(gè)很好的例子。
例如,下面的代碼定義了一個(gè)名為 Author 的類。它最初有兩個(gè)屬性 name 和 age。但我們可以在稍后輕松地添加一個(gè)額外的屬性:
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
然而,任何硬幣都有兩面。這種靈活性會(huì)浪費(fèi)更多內(nèi)存。
因?yàn)?Python 中每個(gè)類的實(shí)例都會(huì)維護(hù)一個(gè)特殊的字典 (__dict__),用于存儲(chǔ)實(shí)例變量。由于字典的底層是基于哈希表的實(shí)現(xiàn),因此字典本身的內(nèi)存效率很低,因此字典會(huì)消耗大量?jī)?nèi)存。
在大多數(shù)情況下,我們不需要在運(yùn)行時(shí)更改實(shí)例的變量或方法,而且在類定義之后,__dict__ 也不會(huì)被更改。因此,我們最好不要維護(hù) __dict__ 字典。
Python 為此提供了一個(gè)神奇的屬性:__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'
就像上面的代碼,我們不能再在運(yùn)行時(shí)添加工作屬性了。因?yàn)?__slots__ 白名單只定義了 name 和 age 兩個(gè)有效屬性。
從理論上講,由于屬性是固定的,Python 不需要為它維護(hù)一個(gè)字典。它只需為 __slots__ 中定義的屬性分配必要的內(nèi)存空間即可。
讓我們寫一個(gè)簡(jiǎn)單的比較程序來看看它是否真的可以這樣工作:
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__,實(shí)例 me_with_slots 沒有__dict__ 字典。與需要保存額外字典的 me 實(shí)例相比,它有效地節(jié)省了內(nèi)存資源。
2. 使用生成器
生成器是 Python 中列表的懶版本。它們的工作方式類似于元素生成工廠:每當(dāng)調(diào)用 next() 方法時(shí)就生成一個(gè)項(xiàng),而不是一次性計(jì)算所有項(xiàng)。因此,在處理大型數(shù)據(jù)集時(shí),它們非常節(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
上面的代碼展示了一個(gè)編寫和使用生成器的基本示例。關(guān)鍵字 yield 是生成器定義的核心。使用它意味著只有在調(diào)用 next() 方法時(shí),才會(huì)產(chǎn)生項(xiàng)目 i。
現(xiàn)在,讓我們比較一下生成器和列表,看看哪一個(gè)更節(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 理解的方括號(hào)轉(zhuǎn)換成小括號(hào),它就會(huì)變成一個(gè)生成器表達(dá)式。這是在 Python 中定義生成器的一種更簡(jiǎn)單的方法:
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,簡(jiǎn)稱 mmap,是一種操作系統(tǒng)級(jí)優(yōu)化。
維基百科:它實(shí)現(xiàn)了需求分頁,因?yàn)槲募?nèi)容不會(huì)立即從磁盤讀取,最初根本不使用物理 RAM。從磁盤實(shí)際讀取的操作是在訪問特定位置后,以一種懶惰的方式進(jìn)行的。
簡(jiǎn)單地說,當(dāng)使用 mmap 技術(shù)對(duì)文件進(jìn)行內(nèi)存映射時(shí),它會(huì)直接在當(dāng)前進(jìn)程的虛擬內(nèi)存空間中創(chuàng)建文件的映射,而不是將整個(gè)文件加載到內(nèi)存中。映射而不是加載整個(gè)文件可以節(jié)省大量?jī)?nèi)存。
看起來很復(fù)雜?幸運(yùn)的是,Python 已經(jīng)提供了使用這種技術(shù)的內(nèi)置模塊,因此我們可以輕松利用它,而無需考慮操作系統(tǒng)級(jí)的實(shí)現(xiàn)。
例如,在 Python 中如何使用 mmap 進(jìn)行文件處理:
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)文件方法甚至切片符號(hào)來處理打開的對(duì)象。
4. 盡量少用全局變量
全局變量具有全局作用域,因此只要程序運(yùn)行,全局變量就會(huì)一直保留在內(nèi)存中。
因此,如果一個(gè)全局變量包含一個(gè)大型數(shù)據(jù)結(jié)構(gòu),它就會(huì)在整個(gè)程序生命周期中占用內(nèi)存,從而可能導(dǎo)致內(nèi)存使用效率低下。
我們應(yīng)該在 Python 代碼中盡量減少全局變量的使用。
5. 利用邏輯操作符
這個(gè)技巧看似微妙,但巧妙地使用它將極大地節(jié)省程序的內(nèi)存使用量。
例如,下面是一個(gè)簡(jiǎn)單的代碼片段,它根據(jù)兩個(gè)函數(shù)返回的布爾值得到最終結(jié)果:
result_a = expensive_function_a() result_b = expensive_function_b() result = result_a if result_a else result_b
上述代碼可以正常運(yùn)行,但它實(shí)際上執(zhí)行了兩個(gè)內(nèi)存不足的函數(shù)。
獲得相同結(jié)果的更聰明的方法如下:
result = expensive_function1() or expensive_function2()
由于邏輯運(yùn)算符遵循短路評(píng)估規(guī)則,如果 expensive_function1() 為 True,則不會(huì)執(zhí)行上述代碼中的 expensive_function2()。這將節(jié)省不必要的內(nèi)存使用。
6. 謹(jǐn)慎選擇數(shù)據(jù)類型
資深的 Python 開發(fā)人員會(huì)謹(jǐn)慎而精確地選擇數(shù)據(jù)類型。因?yàn)樵谀承┣闆r下,使用一種數(shù)據(jù)類型比使用另一種數(shù)據(jù)類型更節(jié)省內(nèi)存。
元組比列表更節(jié)省內(nèi)存
鑒于元組是不可變的(創(chuàng)建后不能更改),它允許 Python 在內(nèi)存分配方面進(jìn)行優(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ù)或所有浮點(diǎn)數(shù)),但列表可以存儲(chǔ)不同類型的對(duì)象,這就不可避免地需要更多內(nèi)存。
因此,如果列表的元素都是同一類型,使用數(shù)組會(huì)更節(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)治語言。有許多強(qiáng)大的第三方模塊和工具提供了更多的數(shù)據(jù)類型,如 NumPy 和 Pandas。
如果我們只需要一個(gè)簡(jiǎn)單的一維數(shù)組,而不需要 NumPy 提供的廣泛功能,那么 Python 的內(nèi)置數(shù)組可能是一個(gè)不錯(cuò)的選擇。
但如果需要進(jìn)行復(fù)雜的矩陣操作,使用 NumPy 提供的數(shù)組可能是所有數(shù)據(jù)科學(xué)家的首選,也可能是最佳選擇。
7. 對(duì)相同字符串應(yīng)用字符串互文技術(shù)
下面的代碼會(huì)讓很多開發(fā)人員感到困惑:
>>> a = 'Y'*4096 >>> b = 'Y'*4096 >>> a is b True >>> c = 'Y'*4097 >>> d = 'Y'*4097 >>> c is d False
我們知道,is 運(yùn)算符用于檢查兩個(gè)變量是否指向內(nèi)存中的同一個(gè)對(duì)象。它與 == 運(yùn)算符不同,后者用于比較兩個(gè)對(duì)象是否具有相同的值。
那么,為什么 a is b 得到的是 True,而 c is d 得到的卻是 False 呢?
如果有幾個(gè)小字符串的值相同,Python 就會(huì)隱式地對(duì)它們進(jìn)行內(nèi)聯(lián),并引用內(nèi)存中的同一個(gè)對(duì)象。
定義小字符串的神奇數(shù)字是 4096。因?yàn)?c 和 d 的長(zhǎng)度都是 4097,所以它們?cè)趦?nèi)存中是兩個(gè)對(duì)象,而不是一個(gè)。不再有隱式字符串互調(diào)。因此,當(dāng)執(zhí)行 c 是 d 時(shí),我們會(huì)得到一個(gè) False。
字符串互調(diào)是一種優(yōu)化內(nèi)存使用的強(qiáng)大技術(shù)。如果我們想顯式地進(jìn)行字符串互調(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)化的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
如何利用Python獲取文本中的電話號(hào)碼實(shí)例代碼
Python的文本處理是經(jīng)常碰到的一個(gè)問題,下面這篇文章主要給大家介紹了關(guān)于如何利用Python獲取文本中的電話號(hào)碼的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-09-09
python 執(zhí)行shell命令并將結(jié)果保存的實(shí)例
今天小編就為大家分享一篇python 執(zhí)行shell命令并將結(jié)果保存的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-05-05
Python Django切換MySQL數(shù)據(jù)庫(kù)實(shí)例詳解
這篇文章主要介紹了Python Django切換MySQL數(shù)據(jù)庫(kù)實(shí)例詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-07-07
python的staticmethod與classmethod實(shí)現(xiàn)實(shí)例代碼
這篇文章主要介紹了python的staticmethod與classmethod實(shí)現(xiàn)實(shí)例代碼,分享了相關(guān)代碼示例,小編覺得還是挺不錯(cuò)的,具有一定借鑒價(jià)值,需要的朋友可以參考下2018-02-02

