Python 課程 – 檔案操作(File I/O)
主題:pickle 序列化
簡介
在日常開發中,資料的永久保存與快速讀寫是非常常見的需求。除了純文字、CSV、JSON 等格式外,Python 內建的 pickle 模組提供了一種「把 Python 物件直接轉成位元組(bytes)再寫入檔案」的方式,讓我們可以毫不費力地保存複雜的資料結構(如自訂類別、嵌套的 list、dict 等)。
使用 pickle 的好處在於:
- 無需手動轉換,Python 會自動處理物件的序列化與反序列化。
- 支援大多數內建與自訂型別,包括函式、類別實例等。
- 讀寫速度相對較快,特別適合在本機或小型專案中暫存中間結果。
然而,pickle 也有安全與相容性的考量,本文將在「常見陷阱與最佳實踐」章節說明。
核心概念
1. 為什麼選擇 pickle 而非純文字格式?
- 結構完整:JSON 只能表達基本型別(字串、數字、list、dict),而
pickle能保存 Python 的完整物件圖。 - 省去手動編碼:自訂類別若要用 JSON 必須自行實作
default方法;pickle只要dump即可。
注意:
pickle產生的檔案是二進位制,非人類可讀,僅適合機器間的資料交換或本機暫存。
2. 基本使用流程
- 匯入模組:
import pickle - 序列化(dump):將物件寫入檔案
- 反序列化(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 檔 | 任意程式碼執行(安全風險) | 只載入可信來源的檔案;若需安全檢查,可改用 json、marshal 或自訂白名單。 |
| 使用預設 protocol(0)導致效能差 | 檔案過大、讀寫慢 | 明確指定 protocol=pickle.HIGHEST_PROTOCOL。 |
| 序列化包含開啟的檔案、socket 等不可序列化物件 | PicklingError 或資料遺失 |
實作 __getstate__ / __setstate__ 排除或轉換這類屬性。 |
| 跨版本 Python 讀寫 | 舊版無法讀取新協議產生的檔案 | 若需跨版本,使用較低的協議(如 2)或改用通用格式(JSON、msgpack)。 |
| 忘記以二進位模式開檔 | UnicodeDecodeError 或資料破損 |
必須使用 "wb" / "rb",切勿使用文字模式。 |
最佳實踐
- 明確指定檔案路徑與模式,使用
with語句確保檔案正確關閉。 - 使用
pickle.HIGHEST_PROTOCOL取得最佳效能與支援。 - 對外部傳遞的資料,若安全性是首要考量,改用
json或MessagePack。 - 在大型資料集,考慮搭配
gzip、bz2或lzma壓縮,以減少磁碟佔用。
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 | 範例說明 |
|---|---|---|
| 機器學習模型暫存 | 訓練好的模型(如 sklearn、numpy 陣列)往往是大型且結構複雜的物件 |
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 物件寫入磁碟或網路,並在需要時快速還原。掌握以下要點,就能安全且有效地運用它:
- 使用二進位模式 (
"wb"/"rb"),並指定最高協議。 - 避免載入不可信的檔案,防止任意程式碼執行。
- 對不可序列化的屬性,實作
__getstate__/__setstate__進行過濾。 - 結合壓縮(
gzip、bz2)或使用json、msgpack作為備選方案,以因應不同的安全與相容需求。
透過本文的範例與最佳實踐,無論是 機器學習模型暫存、遊戲存檔、或是爬蟲快取,你都能自信地使用 pickle 來管理資料,提升開發效率與程式可維護性。祝你在 Python 的檔案操作之路上玩得開心、寫得順利!