當前位置: 妍妍網 > 碼農

提高程式碼效率的6個Python記憶體最佳化技巧

2024-01-25碼農

來源:Deephub IMBA

當計畫變得越來越大時,有效地管理計算資源是一個不可避免的需求。Python與C或c++等低階語言相比,似乎不夠節省記憶體。

但是其實有許多方法可以顯著最佳化Python程式的記憶體使用,這些方法可能在實際套用中並沒有人註意,所以本文將重點介紹Python的內建機制,掌握它們將大大提高Python編程技能。

首先在進行記憶體最佳化之前,我們首先要檢視記憶體的使用情況

分配了多少記憶體?

有幾種方法可以在Python中獲取物件的大小。可以使用sys.getsizeof()來獲取物件的確切大小,使用objgraph.show_refs()來視覺化物件的結構,或者使用psutil.Process().memory_info()。RSS獲取當前分配的所有記憶體。

>>> import numpy as np
>>> import sys
>>> import objgraph
>>> import psutil
>>> import pandas as pd


>>> ob = np.ones((1024, 1024, 1024, 3), dtype=np.uint8)

### Check object 'ob' size
>>> sys.getsizeof(ob) / (1024 * 1024)
3072.0001373291016

### Check current memory usage of whole process (include ob and installed packages, ...)
>>> psutil.Process().memory_info().rss / (1024 * 1024)
3234.19140625

### Check structure of 'ob' (Useful for class object)
>>> objgraph.show_refs([ob], filename='sample-graph.png')

### Check memory for pandas.DataFrame
>>> from sklearn.datasets import load_boston
>>> data = load_boston()
>>> data = pd.DataFrame(data['data'])
>>> print(data.info(verbose=False, memory_usage='deep'))
< class 'pandas.core.frame.DataFrame'>
RangeIndex: 506 entries, 0 to 505
Columns: 13 entries, 0 to 12
dtypes: float64(13)
memory usage: 51.5 KB

### Check memory for pandas.Series
>>> data[0].memory_usage(deep=True) # deep=True to include all the memory used by underlying parts that construct the pd.Series
4176

這樣我們才能根據物件的記憶體占用來檢視實際的最佳化結果

__slots__

Python作為一種動態型別語言,在物件導向方面具有更大的靈活性。在執行時可以向Python類添加額外內容和方法的能力。

例如,下面的程式碼定義了一個名為Author的類。最初它有兩個內容name和age。但是我們以後可以很容易地添加一個額外的job:

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

但是這種靈活性在底層浪費了更多記憶體。

因為Python中每個類的例項都維護一個特殊的字典( _ _ 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'

白名單只定義了兩個有效的內容name和age。由於內容是固定的,Python不需要為它維護字典,只為 _ _ slots _ _ 中定義的內容分配必要的記憶體空間。

下面我們做一個簡單的比較:

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__'

可以看到 152 和 48 明顯節省了記憶體。

Generators

生成器是Python中列表的惰性求值版本。每當呼叫next()方法時生成一個項,而不是一次計算所有項。所以它們在處理大型數據集時非常節省記憶體。

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

上面的程式碼顯示了一個編寫和使用生成器的基本範例。關鍵字yield是生成器定義的核心。套用它意味著只有在呼叫next()方法時才會產生項i。

讓我們比較一個生成器和一個列表,看看哪個更節省記憶體:

mport 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

可以看到使用生成器可以顯著節省記憶體使用。如果我們將列表推導式的方括弧轉換成圓括弧,它將成為生成器運算式。這是在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

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

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

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

Python已經提供了用於使用此技術的內建模組,因此我們可以輕松地利用它,而無需考慮作業系統級別的實作。

以下是如何在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使記憶體對映檔I/O技術的使用變得方便。我們所需要做的只是套用mmap.mmap()方法,然後使用標準檔方法甚至切片符號處理開啟的物件。

選擇適當的數據型別

開發人員應仔細而精確地選擇數據型別。因為在某些情況下,使用一種數據型別比使用另一種數據型別更節省記憶體。

1、元組比列表更節省記憶體

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

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比列表使用更少的記憶體,如果建立後不需要更改數據,我們應該選擇元組而不是列表。

2、陣列比列表更節省記憶體

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

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

另外:Python是數據科學的主導語言。有許多強大的第三方模組和工具提供更多的數據型別,如NumPy和Pandas。如果我們只需要一個簡單的一維數位陣列,而不需要NumPy提供的廣泛功能,那麽Python的內建陣列是一個不錯的選擇。但當涉及到復雜的矩陣操作時,使用NumPy提供的陣列是所有數據科學家的首選,也可能是最佳選擇。

字串駐留

看看下面的程式碼:

>>> a = 'Y'*4096
>>> b = 'Y'*4096
>>> a is b
True
>>> c = 'Y'*4097
>>> d = 'Y'*4097
>>> c is d
False

為什麽a是b是真,而c是d是假呢?

這在Python中被稱作字串駐留(string interning).如果有幾個值相同的小字串,它們將被Python隱式地儲存並在記憶體中並參照相同的物件。定義小字串閾值數位是4096。

由於c和d的長度為4097,因此它們是記憶體中的兩個物件而不是一個物件,不再隱式駐留字串。所以當執行c = d時,我們得到一個False。

駐留是一種最佳化記憶體使用的強大技術。如果我們想要顯式地使用它可以使用sys.intern()方法:

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


作者:Yang Zhou

加入知識星球【我們談論數據科學】

600+小夥伴一起學習!