🐍 Python 精簡教學:用 jsonpickle 儲存與還原多型物件!
在 Python 中,我們常需要把自訂物件轉成 JSON 儲存或傳輸。不過標準的 json 模組只能處理 dict、list、str、int 等基本型別,遇到類別(尤其是多型)時就 GG 😵。
這時候,jsonpickle 就是你的好幫手!
✅ jsonpickle 是什麼?
jsonpickle 是一個 Python 套件,可以將 任意 Python 物件(包含類別、巢狀、多型)序列化成 JSON,並支援還原。
它的效果很像 Java 的 @JsonTypeInfo(use = CLASS) 或 Python 的 pickle,但是 JSON 格式,更通用!
🚀 快速上手:一分鐘搞懂 jsonpickle
✨ 安裝
pip install jsonpickle
🐶 假設我們有以下類別結構:
class Animal:
def __init__(self, name):
self.name = name
class Dog(Animal):
def __init__(self, name, bark=True):
super().__init__(name)
self.bark = bark
🔁 序列化 + 反序列化
import jsonpickle
dog = Dog("Buddy")
# 序列化成 JSON 字串
json_str = jsonpickle.encode(dog)
print(json_str)
# 反序列化回原本的 Dog 物件
obj = jsonpickle.decode(json_str)
print(type(obj), obj.__dict__)
📦 輸出 JSON 範例
{
"py/object": "__main__.Dog",
"name": "Buddy",
"bark": true
}
這裡的 "py/object" 就是類似 Java 的 "@class",自動包含完整類型資訊,還原時會用 import 方式動態載入類別。
🧠 使用情境
| 使用場景 | 是否適合 |
|---|---|
| 儲存/還原動態型別資料 | ✅ 非常適合 |
| 序列化巢狀資料或循環引用 | ✅ 處理得很好 |
| 跨語言(送 JSON 給前端) | ❌ 不推薦,格式太特殊 |
| 高安全性場景(接受用戶輸入) | ⚠️ 謹慎使用,勿 decode 不可信 JSON |
🛡️ 安全提醒!
因為 jsonpickle.decode() 會依照 py/object 動態建立類別實例,千萬不要用來反序列化不可信來源的 JSON!
如需與外部系統互動,建議使用 dict() + json 手動序列化或用 pydantic。
🏁 結語
| 你要的功能 | 用 jsonpickle ✅ |
|---|---|
| 序列化/反序列化自訂類別 | ✅ |
| 保留類型資訊 | ✅ 自動加 "py/object" |
| 快速還原 Python 多型物件 | ✅ 一行搞定 |
簡單一句話:jsonpickle 是 Python 版的 @JsonTypeInfo(use = CLASS),超適合處理多型、巢狀與類別封裝物件!
📝jsonpickle 與 @dataclass 交互行為
1. 核心概念
-
Dataclass:負責定義資料結構(Schema),簡化類別宣告。
-
jsonpickle:負責物件序列化(Serialization),將 Python 物件轉為 JSON 字串,並能完整還原(包含類別資訊)。
-
交互結論:兩者原生相容,無需額外配置。
jsonpickle針對dataclass的特殊欄位(如init=False)也能正確處理。
2. 關鍵行為測試
測試場景
我們定義一個包含以下特性的 Dataclass:
-
一般欄位:
id,name -
default_factory:tags(預設為空 list) -
init=False:created_at(不透過 init 傳入,而是內部產生)
import jsonpickle
from dataclasses import dataclass, field
from datetime import datetime
import time
@dataclass
class Task:
id: int
name: str
# 特性 A: default_factory (預設值工廠)
tags: list[str] = field(default_factory=list)
# 特性 B: init=False (非建構子參數,通常由 __post_init__ 產生)
created_at: float = field(init=False)
def __post_init__(self):
self.created_at = time.time()
# --- 1. 建立物件 ---
task = Task(id=1, name="Data Cleanup")
task.tags.append("urgent")
#此時 task.created_at 已經由 __post_init__ 自動生成
print(f"原始物件: {task}")
# --- 2. 序列化 (Encode) ---
json_str = jsonpickle.encode(task, indent=2)
print(f"\nJSON (儲存狀態):\n{json_str}")
# --- 3. 反序列化 (Decode) ---
restored_task = jsonpickle.decode(json_str)
print(f"\n還原物件: {restored_task}")
觀察結果
JSON 輸出會包含所有欄位,包括 init=False 的欄位:
{
"py/object": "__main__.Task",
"id": 1,
"name": "Data Cleanup",
"tags": ["urgent"],
"created_at": 1708320000.123 <-- 狀態被完整保存
}
3. 重要機制詳解 (The "Gotchas")
Q1: field(init=False) 的欄位會被儲存嗎?
-
答案:會。
-
原因:
jsonpickle序列化的是 "物件當下的狀態" (__dict__),而不是 "建構函式的參數"。只要該欄位在物件中存在(即使是在__post_init__中賦值的),就會被存下來。
Q2: 反序列化時,會重新執行 __init__ 或 __post_init__ 嗎?
-
答案:不會。 (這是最重要的觀念)
-
機制:
jsonpickle還原物件時,是創建一個空物件,然後將 JSON 裡的數值直接填入__dict__。 -
影響:
-
如果你的
created_at是透過time.time()在__post_init__生成的,還原時不會變成現在的時間,而是保持 "當初儲存時的時間"。 -
這保證了資料的一致性(Snapshot)。
-
Q3: default_factory 的行為?
-
答案:正常運作。
-
物件建立時,List 被初始化。序列化時,List 裡的內容(例如
['urgent'])被存入 JSON。還原時,這些內容直接被讀回來。
4. 最佳實踐建議
-
適合場景:
-
複雜設定檔:例如你的 ETL Flow 設定,包含巢狀結構。
-
暫存/快取 (Caching):將物件狀態存入 Redis 或檔案,稍後讀取繼續執行。
-
除錯 (Debugging):將出錯的物件 Dump 下來分析。
-
-
不適合場景:
- 跨語言傳輸:因為 JSON 中包含 Python 特有的
py/object標籤,Java 或 JS 讀取時會覺得怪怪的(雖然可以設unpicklable=False來拿掉標籤,但就無法自動還原回物件了)。
- 跨語言傳輸:因為 JSON 中包含 Python 特有的
-
安全性提醒:
- 永遠不要對 不可信來源 的 JSON 字串執行
jsonpickle.decode(),這與pickle模組有相同的安全風險(可能導致惡意程式碼執行)。
- 永遠不要對 不可信來源 的 JSON 字串執行
5. 總結圖表
| Dataclass 特性 | jsonpickle 行為 | 備註 |
|---|---|---|
| 一般欄位 | ✅ 保存 & 還原 | 最基本功能 |
field(default=...) |
✅ 保存 & 還原 | 如果值與預設值不同,存當下的值 |
field(default_factory=...) |
✅ 保存 & 還原 | List/Dict 內容會被完整保留 |
field(init=False) |
✅ 保存 & 還原 | 關鍵:還原時不重新計算,保留最後狀態 |
ClassVar |
❌ 忽略 | 屬於類別屬性,不屬於實例狀態 |