如何在 Python 中最佳化記憶體使用量:從基礎到應用的完整指南

目次

1. 前言

目標讀者

本文主要針對經常使用 Python 的初學者到中階使用者。對於想要了解並最佳化程式記憶體使用量的讀者尤其有幫助。

文章目的

本篇文章的目的是:

  1. 理解 Python 記憶體管理的機制。
  2. 學習測量記憶體使用量的具體方法。
  3. 掌握降低記憶體使用量的最佳化技巧。

了解這些內容將有助於提升 Python 程式的效能。

2. Python 的記憶體管理基礎

記憶體管理的機制

在 Python 中,記憶體管理主要透過「參考計數(Reference Counting)」與「垃圾回收(Garbage Collection)」這兩種機制來進行。

參考計數

參考計數是指計算每個物件被引用的次數。
在 Python 中,當一個物件被建立時,參考計數會設定為 1。每當其他變數引用該物件時,計數會增加;當引用解除時,計數會減少。當參考計數為 0 時,該物件會自動從記憶體中釋放。

程式碼範例
import sys

a = [1, 2, 3]  ## 建立一個 list 物件
print(sys.getrefcount(a))  ## 初始參考計數(通常為 2,包含內部引用)

b = a  ## 另一個變數引用相同物件
print(sys.getrefcount(a))  ## 參考計數增加

del b  ## 移除引用
print(sys.getrefcount(a))  ## 參考計數減少

垃圾回收

垃圾回收(Garbage Collection, GC)是用來回收無法透過參考計數自動釋放的記憶體,特別是循環引用的情況。Python 內建的垃圾回收器會定期執行,自動清除不再需要的物件。

垃圾回收器擅長處理循環引用的偵測與釋放,在以下情況特別有用:

class Node:
    def __init__(self):
        self.next = None

## 循環引用範例
a = Node()
b = Node()
a.next = b
b.next = a

## 此情況下參考計數不會歸零,記憶體不會被釋放

若想手動操作垃圾回收器,可使用 gc 模組進行控制:

import gc

## 強制執行垃圾回收
gc.collect()

記憶體洩漏的風險

雖然 Python 的記憶體管理功能強大,但仍非萬無一失。特別是在以下情況中,可能會導致記憶體洩漏的風險:

  1. 存在循環引用 且垃圾回收功能被停用時。
  2. 長時間執行的程式中,未使用的物件仍留在記憶體中。

為避免這些問題,應採取避免循環引用的設計,並明確刪除不再需要的物件。

本節小結

  • Python 的記憶體管理主要透過「參考計數」與「垃圾回收」兩種機制。
  • 垃圾回收對於解決循環引用特別有幫助,但良好的程式設計可有效避免不必要的記憶體消耗。
  • 下一節將說明如何實際測量記憶體使用量的方法。

3. 如何確認 Python 的記憶體使用量

基本方法

使用 sys.getsizeof() 確認物件大小

透過 Python 標準函式庫中的 sys 模組提供的 getsizeof() 函數,可以取得任意物件所佔用的記憶體大小(以位元組為單位)。

程式碼範例
import sys

## 查看各種物件的記憶體大小
x = 42
y = [1, 2, 3, 4, 5]
z = {"a": 1, "b": 2}

print(f"x 的大小: {sys.getsizeof(x)} 位元組")
print(f"y 的大小: {sys.getsizeof(y)} 位元組")
print(f"z 的大小: {sys.getsizeof(z)} 位元組")
注意事項
  • sys.getsizeof() 只能取得該物件本身的大小,不包括其所參考的其他物件(如 list 中的元素)。
  • 若需精確測量大型物件的實際記憶體使用量,可能需要其他輔助工具。

使用記憶體分析工具

使用 memory_profiler 逐函數測量記憶體

memory_profiler 是一個外部函式庫,可詳細測量 Python 程式中每個函數的記憶體使用情況。能幫助你輕鬆找出記憶體消耗大的部分。

安裝方式

首先,請安裝 memory_profiler

pip install memory-profiler
使用方式

使用 @profile 裝飾器可針對函數進行記憶體測量。

from memory_profiler import profile

@profile
def example_function():
    a = [i for i in range(10000)]
    b = {i: i**2 for i in range(1000)}
    return a, b

if __name__ == "__main__":
    example_function()

執行程式時,請使用以下指令:

python -m memory_profiler your_script.py
輸出範例
Line ##    Mem usage    Increment   Line Contents
------------------------------------------------
     3     13.1 MiB     13.1 MiB   @profile
     4     16.5 MiB      3.4 MiB   a = [i for i in range(10000)]
     5     17.2 MiB      0.7 MiB   b = {i: i**2 for i in range(1000)}

使用 psutil 監控整體記憶體使用量

psutil 是一個功能強大的函式庫,可監控整個程序的記憶體使用情況,非常適合用於監測整體的資源消耗。

安裝方式

使用以下指令安裝:

pip install psutil
使用方式
import psutil

process = psutil.Process()
print(f"整體程序的記憶體使用量: {process.memory_info().rss / 1024**2:.2f} MB")
主要特色
  • 可取得當前程序的記憶體使用量(以位元組為單位)。
  • 可用於監控效能並找出最佳化的切入點。

進階記憶體追蹤

使用 tracemalloc 追蹤記憶體配置

使用 Python 標準函式庫的 tracemalloc 模組,可以追蹤記憶體配置來源,分析哪些程式碼區塊使用了最多記憶體。

使用方式
import tracemalloc

## 開始追蹤記憶體
tracemalloc.start()

## 模擬記憶體使用
a = [i for i in range(100000)]

## 顯示記憶體使用狀況
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics("lineno")

print("[記憶體使用情況]")
for stat in top_stats[:5]:
    print(stat)
主要用途
  • 找出記憶體配置量高的程式區段。
  • 比較不同處理流程,找出最佳化空間。

本節小結

  • Python 的記憶體使用量可透過多種工具來檢測,從基本的 sys.getsizeof() 到專業的 memory_profilerpsutil 等。
  • 若你的程式對效能或資源消耗要求較高,請選擇合適工具來進行精細管理。
  • 下一節將介紹如何實際進行記憶體最佳化。

4. 如何最佳化記憶體使用量

選擇高效的資料結構

將 list 替換為 generator

雖然 list comprehension 很方便,但在處理大量資料時會佔用大量記憶體。若改用 generator,則可按需產生資料,大幅降低記憶體使用量。

程式碼範例
## 使用 list
list_data = [i**2 for i in range(1000000)]
print(f"list 記憶體大小: {sys.getsizeof(list_data) / 1024**2:.2f} MB")

## 使用 generator
gen_data = (i**2 for i in range(1000000))
print(f"generator 記憶體大小: {sys.getsizeof(gen_data) / 1024**2:.2f} MB")

透過使用 generator,可以顯著降低記憶體的使用量。

使用 collections.defaultdict 作為字典的替代

雖然 Python 的字典非常好用,但處理大型資料時會消耗較多記憶體。使用 collections.defaultdict 可讓你更有效率地設定預設值,簡化處理流程。

程式碼範例
from collections import defaultdict

## 一般字典
data = {}
data["key"] = data.get("key", 0) + 1

## 使用 defaultdict
default_data = defaultdict(int)
default_data["key"] += 1

管理不再需要的物件

使用 del 明確刪除

在 Python 中,可以手動刪除不再需要的物件,減輕垃圾回收器的負擔。

程式碼範例
## 刪除不再使用的變數
a = [1, 2, 3]
del a

刪除後,變數 a 所佔用的記憶體將被釋放。

使用垃圾回收器

你可以透過 gc 模組來手動執行垃圾回收,以釋放因循環參照而未被自動回收的記憶體。

程式碼範例
import gc

## 執行垃圾回收
gc.collect()

活用外部函式庫進行最佳化

善用 NumPy 與 Pandas

NumPy 和 Pandas 是針對大量數值資料而設計的高效資料處理函式庫,可顯著節省記憶體。

NumPy 使用範例
import numpy as np

## Python list
data_list = [i for i in range(1000000)]
print(f"list 記憶體大小: {sys.getsizeof(data_list) / 1024**2:.2f} MB")

## NumPy 陣列
data_array = np.arange(1000000)
print(f"NumPy 陣列記憶體大小: {data_array.nbytes / 1024**2:.2f} MB")

可以看出,NumPy 陣列比 list 更具記憶體效率。

預防記憶體洩漏

為防止記憶體洩漏,請留意以下幾點:

  1. 避免循環引用
    設計時避免物件互相參考。
  2. 控制變數作用範圍
    注意函式與類別中的變數作用範圍,避免留下不必要的物件。

本節小結

  • 若要最佳化記憶體使用量,應選擇高效資料結構,並刪除不再使用的物件。
  • 活用如 NumPy、Pandas 等外部函式庫可進一步提升效率。
  • 下一節將說明實務上常見問題的解決技巧。
侍エンジニア塾

5. 疑難排解(Troubleshooting)

應對記憶體使用量突然飆升的情況

調整垃圾回收器

如果垃圾回收器未正常運作,可能導致不必要的記憶體無法釋放,使記憶體使用量急劇上升。此時可以使用 gc 模組進行調整。

程式碼範例
import gc

## 查看垃圾回收器的觸發門檻值
print(gc.get_threshold())

## 手動執行垃圾回收
gc.collect()

## 調整垃圾回收器設定(例如:變更門檻)
gc.set_threshold(700, 10, 10)

重新檢討物件的生命週期

有時候某些物件即使已不再需要,卻仍留在記憶體中。這時應重新檢視物件的生命週期,並在適當時機將其刪除,以釋放資源。

因循環引用導致的記憶體洩漏

問題概要

當兩個或以上的物件彼此互相引用時,就會形成「循環引用」,這種情況下參考計數無法歸零,可能導致垃圾回收器無法釋放該記憶體。

解決方案
  • 使用弱參考(weakref 模組)來避免循環引用。
  • 手動執行垃圾回收以釋放記憶體。
程式碼範例
import weakref

class Node:
    def __init__(self, name):
        self.name = name
        self.next = None

a = Node("A")
b = Node("B")

## 使用弱參考來避免循環引用
a.next = weakref.ref(b)
b.next = weakref.ref(a)

當記憶體分析工具無法運作時

memory_profiler 出現錯誤

有時使用 memory_profiler 時,@profile 裝飾器可能無法正常運作,通常是因為腳本執行方式不正確所導致。

解決方法
  1. 使用 -m memory_profiler 選項來執行腳本:
python -m memory_profiler your_script.py
  1. 確認 @profile 有正確套用在目標函數上。

psutil 出現錯誤

如果 psutil 無法正常取得記憶體資訊,可能是版本不兼容或執行環境出現問題。

解決方法
  1. 確認 psutil 為最新版本,並進行升級:
pip install --upgrade psutil
  1. 確認你是以正確的方式取得程序資訊:
import psutil
process = psutil.Process()
print(process.memory_info())

應對記憶體不足錯誤(MemoryError)

問題概要

當處理大量資料時,可能會遇到記憶體不足(MemoryError)的問題。

解決方法
  • 減少資料量
    刪除不必要的資料,並使用更有效率的資料結構。
## 使用 generator
large_data = (x for x in range(10**8))
  • 分段處理
    將資料切分為小區塊來處理,可減少一次性佔用的記憶體。
for chunk in range(0, len(data), chunk_size):
    process_data(data[chunk:chunk + chunk_size])
  • 利用外部儲存
    將資料儲存至磁碟而非記憶體(如 SQLite、HDF5 等)。

本節小結

  • 透過調整垃圾回收器與物件生命週期管理,可有效控制記憶體使用量。
  • 遇到循環引用或工具錯誤時,可使用弱參考或重新設定工具進行修正。
  • 面對記憶體不足時,可採用資料壓縮、分段處理或外部儲存等策略來應對。

6. 實用範例:在 Python 腳本中測量記憶體使用量

本節將結合前面介紹的工具與技巧,透過實際的 Python 腳本範例,示範如何分析與最佳化記憶體使用量。透過這些範例,你將學會如何診斷與改善記憶體效能問題。

範例情境:比較 list 與 dict 的記憶體使用量

程式碼範例

下列腳本使用 sys.getsizeof()memory_profiler 來測量 list 與 dict 的記憶體使用情況。

import sys
from memory_profiler import profile

@profile
def compare_memory_usage():
    ## 建立 list
    list_data = [i for i in range(100000)]
    print(f"list 的記憶體使用量: {sys.getsizeof(list_data) / 1024**2:.2f} MB")

    ## 建立 dict
    dict_data = {i: i for i in range(100000)}
    print(f"dict 的記憶體使用量: {sys.getsizeof(dict_data) / 1024**2:.2f} MB")

    return list_data, dict_data

if __name__ == "__main__":
    compare_memory_usage()

執行步驟

  1. 若尚未安裝 memory_profiler,請先執行以下指令:
pip install memory-profiler
  1. 使用 memory_profiler 執行腳本:
python -m memory_profiler script_name.py

輸出結果範例

Line ##    Mem usage    Increment   Line Contents
------------------------------------------------
     5     13.2 MiB     13.2 MiB   @profile
     6     17.6 MiB      4.4 MiB   list_data = [i for i in range(100000)]
     9     22.2 MiB      4.6 MiB   dict_data = {i: i for i in range(100000)}

list 的記憶體使用量: 0.76 MB
dict 的記憶體使用量: 3.05 MB

從結果可以看出,dict 的記憶體使用量遠高於 list,這有助於在設計程式時選擇更合適的資料結構。

範例情境:監控整個程序的記憶體使用量

程式碼範例

下列腳本使用 psutil 即時監控整個 Python 程序的記憶體使用情況。

import psutil
import time

def monitor_memory_usage():
    process = psutil.Process()
    print(f"初始記憶體使用量: {process.memory_info().rss / 1024**2:.2f} MB")

    ## 模擬記憶體消耗
    data = [i for i in range(10000000)]
    print(f"處理中記憶體使用量: {process.memory_info().rss / 1024**2:.2f} MB")

    del data
    time.sleep(2)  ## 等待垃圾回收器執行
    print(f"刪除資料後記憶體使用量: {process.memory_info().rss / 1024**2:.2f} MB")

if __name__ == "__main__":
    monitor_memory_usage()

執行步驟

  1. 如果尚未安裝 psutil,請先執行以下指令:
pip install psutil
  1. 執行該腳本:
python script_name.py

輸出結果範例

初始記憶體使用量: 12.30 MB
處理中記憶體使用量: 382.75 MB
刪除資料後記憶體使用量: 13.00 MB

這個範例展示了大量資料會消耗記憶體,以及在刪除物件後如何釋放記憶體。

本節重點

  • 測量記憶體使用量時,可靈活運用 sys.getsizeof()memory_profilerpsutil 等工具。
  • 可視化記憶體消耗情況有助於發現瓶頸並進行效能優化。

7. 總結與下一步行動

本文重點整理

  1. Python 的記憶體管理基礎
  • Python 使用「參考計數」與「垃圾回收」兩種機制來自動管理記憶體。
  • 為避免循環引用造成的記憶體洩漏,需採取適當的程式設計方式。
  1. 確認記憶體使用量的方法
  • 使用 sys.getsizeof() 可查看單一物件的記憶體大小。
  • 使用 memory_profilerpsutil 等工具可進行函數級與整體程序級的詳細測量。
  1. 最佳化記憶體使用量的方法
  • 使用 generator 或高效資料結構(例如 NumPy 陣列)可降低記憶體消耗。
  • 刪除不再使用的物件與適時使用垃圾回收器有助於防止記憶體洩漏。
  1. 實務應用範例
  • 透過實際的程式碼範例,我們學習了如何測量與分析記憶體使用狀況。
  • 比較 list 與 dict 的記憶體差異、監控整體記憶體使用量等技巧都可直接應用於開發中。

下一步行動

  1. 實際應用於你的專案
  • 將本文介紹的技巧與工具整合至你日常的 Python 專案中。
  • 例如,在處理大量資料的腳本中使用 memory_profiler 來找出記憶體使用過高的區段。
  1. 進一步學習進階記憶體管理
  1. 善用外部工具與服務
  • 在大型專案中,可考慮使用 py-spyPyCharm 的 profiler 工具進行更深入的分析。
  • 在雲端環境下執行程式時,也可以整合 AWS、Google Cloud 等平台所提供的資源監控工具。
  1. 持續進行程式碼檢討與改進
  • 若你是團隊開發的一員,可在 code review 中納入記憶體效能的討論,發掘潛在的優化點。
  • 培養注重記憶體效率的開發習慣,將為長期開發效能帶來正面影響。

結語

妥善管理 Python 程式的記憶體使用,不僅能提升效能,也代表開發者具備更高層次的工程實力。希望你能透過本篇文章的內容,實際應用於專案中,並持續深化理解與技巧,讓你的程式更加高效且穩定!

RUNTEQ(ランテック)|超実戦型エンジニア育成スクール