本文 AI 產出,尚未審核

Python 課程 – 檔案操作(File I/O)

主題:pickle 序列化


簡介

在日常開發中,資料的永久保存與快速讀寫是非常常見的需求。除了純文字、CSV、JSON 等格式外,Python 內建的 pickle 模組提供了一種「把 Python 物件直接轉成位元組(bytes)再寫入檔案」的方式,讓我們可以毫不費力地保存複雜的資料結構(如自訂類別、嵌套的 list、dict 等)。

使用 pickle 的好處在於:

  1. 無需手動轉換,Python 會自動處理物件的序列化與反序列化。
  2. 支援大多數內建與自訂型別,包括函式、類別實例等。
  3. 讀寫速度相對較快,特別適合在本機或小型專案中暫存中間結果。

然而,pickle 也有安全與相容性的考量,本文將在「常見陷阱與最佳實踐」章節說明。


核心概念

1. 為什麼選擇 pickle 而非純文字格式?

  • 結構完整:JSON 只能表達基本型別(字串、數字、list、dict),而 pickle 能保存 Python 的完整物件圖。
  • 省去手動編碼:自訂類別若要用 JSON 必須自行實作 default 方法;pickle 只要 dump 即可。

注意pickle 產生的檔案是二進位制,非人類可讀,僅適合機器間的資料交換或本機暫存。


2. 基本使用流程

  1. 匯入模組import pickle
  2. 序列化(dump):將物件寫入檔案
  3. 反序列化(load):從檔案讀回物件

以下示範最簡單的寫入與讀取:

import pickle

data = {"name": "Alice", "age": 30, "scores": [85, 92, 78]}

# --- 寫入檔案 (序列化) ---
with open("data.pkl", "wb") as f:          # 使用二進位寫入模式
    pickle.dump(data, f)                   # 把 data 轉成 bytes 並寫入

# --- 讀取檔案 (反序列化) ---
with open("data.pkl", "rb") as f:          # 使用二進位讀取模式
    restored = pickle.load(f)              # 從檔案還原物件
print(restored)                            # {'name': 'Alice', 'age': 30, 'scores': [85, 92, 78]}

3. 多物件寫入與 pickle.dump vs pickle.dumps

pickle.dump 直接寫入檔案;若要先取得位元組再自行處理,可使用 pickle.dumps

import pickle

obj1 = [1, 2, 3]
obj2 = {"a": 10, "b": 20}

# 取得 bytes
bytes1 = pickle.dumps(obj1)
bytes2 = pickle.dumps(obj2)

# 合併寫入同一檔案,使用 pickle.HIGHEST_PROTOCOL 提升效能
with open("multi.pkl", "wb") as f:
    f.write(bytes1)
    f.write(bytes2)          # 直接寫入 bytes,之後讀取要自行切割

# 讀回時,要依序使用 pickle.loads
with open("multi.pkl", "rb") as f:
    raw = f.read()
    # 假設我們知道兩個物件的長度,或使用 pickletools 解析

實務技巧:若需要在同一檔案中存多筆資料,建議使用 pickle.dump 多次寫入,而非自行拼接 bytes,因為 pickle.load 會自動辨識每筆物件的結尾。

with open("records.pkl", "wb") as f:
    for i in range(5):
        pickle.dump({"index": i, "value": i*i}, f)

# 讀回所有記錄
records = []
with open("records.pkl", "rb") as f:
    while True:
        try:
            records.append(pickle.load(f))
        except EOFError:               # 檔案結尾
            break
print(records)   # [{'index': 0, 'value': 0}, ...]

4. 自訂類別的序列化

pickle 能自動序列化大多數類別,但若類別內含不可序列化的屬性(如開啟的檔案、socket),需要自行排除或實作 __getstate__ / __setstate__

import pickle

class Person:
    def __init__(self, name, age, file_handle=None):
        self.name = name
        self.age = age
        self.file_handle = file_handle   # 不可序列化的屬性

    # 移除不可序列化的屬性
    def __getstate__(self):
        state = self.__dict__.copy()
        del state['file_handle']
        return state

    # 復原時重新建立或留空
    def __setstate__(self, state):
        self.__dict__.update(state)
        self.file_handle = None

p = Person("Bob", 45, open("dummy.txt", "w"))
with open("person.pkl", "wb") as f:
    pickle.dump(p, f)

with open("person.pkl", "rb") as f:
    p2 = pickle.load(f)
print(p2.name, p2.age, p2.file_handle)   # Bob 45 None

5. protocol 參數與效能

pickle.dump/dumps 接受 protocol 參數,決定序列化的格式與效能。常用的選項:

Protocol Python 版本 特點
0 2.0+ 人類可讀的文字格式,兼容最舊版本
1 2.3+ 老式二進位格式
2 2.3+ 支援較大資料,較快
3 3.0+ 支援大型 Python 3 物件
4 3.4+ 支援巨型集合、外部化資料
5 3.8+ 支援 out‑of‑band data (PEP 574)

在新專案中,建議使用 pickle.HIGHEST_PROTOCOL,讓 Python 自動選擇最高可用的協議:

with open("fast.pkl", "wb") as f:
    pickle.dump(data, f, protocol=pickle.HIGHEST_PROTOCOL)

常見陷阱與最佳實踐

陷阱 可能的後果 建議的解決方式
直接載入不可信的 pickle 檔 任意程式碼執行(安全風險) 只載入可信來源的檔案;若需安全檢查,可改用 jsonmarshal 或自訂白名單。
使用預設 protocol(0)導致效能差 檔案過大、讀寫慢 明確指定 protocol=pickle.HIGHEST_PROTOCOL
序列化包含開啟的檔案、socket 等不可序列化物件 PicklingError 或資料遺失 實作 __getstate__ / __setstate__ 排除或轉換這類屬性。
跨版本 Python 讀寫 舊版無法讀取新協議產生的檔案 若需跨版本,使用較低的協議(如 2)或改用通用格式(JSON、msgpack)。
忘記以二進位模式開檔 UnicodeDecodeError 或資料破損 必須使用 "wb" / "rb",切勿使用文字模式。

最佳實踐

  1. 明確指定檔案路徑與模式,使用 with 語句確保檔案正確關閉。
  2. 使用 pickle.HIGHEST_PROTOCOL 取得最佳效能與支援。
  3. 對外部傳遞的資料,若安全性是首要考量,改用 jsonMessagePack
  4. 在大型資料集,考慮搭配 gzipbz2lzma 壓縮,以減少磁碟佔用。
import pickle, gzip

data = {"big": list(range(1000000))}

# 壓縮寫入
with gzip.open("big_data.pkl.gz", "wb") as f:
    pickle.dump(data, f, protocol=pickle.HIGHEST_PROTOCOL)

# 壓縮讀回
with gzip.open("big_data.pkl.gz", "rb") as f:
    restored = pickle.load(f)
print(len(restored["big"]))   # 1000000

實際應用場景

場景 為何適合使用 pickle 範例說明
機器學習模型暫存 訓練好的模型(如 sklearnnumpy 陣列)往往是大型且結構複雜的物件 pickle.dump(model, open("model.pkl","wb"))
遊戲存檔 遊戲狀態可能包含自訂類別、座標、背包清單等 pickle.dump(game_state, f)
爬蟲快取 把已抓取的 HTML 解析結果快取起來,避免重複下載 pickle.dump(cache_dict, cache_file)
資料科學工作流程的中間結果 例如大量資料清理後的 DataFrame,使用 pandas.to_pickle / read_pickle df.to_pickle("cleaned.pkl")
分散式任務排程 把任務參數與回呼函式序列化後傳遞給工作節點 pickle.dumps(task_obj)

小提醒pandas 已提供 to_pickle / read_pickle 包裝,使用時可直接呼叫,免除手動 pickle.dump 的步驟。


總結

pickle 是 Python 內建、功能強大的物件序列化工具,讓我們可以輕鬆把任意 Python 物件寫入磁碟或網路,並在需要時快速還原。掌握以下要點,就能安全且有效地運用它:

  1. 使用二進位模式 ("wb" / "rb"),並指定最高協議。
  2. 避免載入不可信的檔案,防止任意程式碼執行。
  3. 對不可序列化的屬性,實作 __getstate__ / __setstate__ 進行過濾。
  4. 結合壓縮gzipbz2)或使用 jsonmsgpack 作為備選方案,以因應不同的安全與相容需求。

透過本文的範例與最佳實踐,無論是 機器學習模型暫存、遊戲存檔、或是爬蟲快取,你都能自信地使用 pickle 來管理資料,提升開發效率與程式可維護性。祝你在 Python 的檔案操作之路上玩得開心、寫得順利!