Python使用Pickle模塊序列化對象的完整指南
1. 什么是 Pickle?不僅僅是“打包”那么簡單
在 Python 的世界里,我們經(jīng)常需要處理各種各樣的對象:從簡單的列表、字典,到復雜的自定義類實例。當程序運行時,這些對象都活在內(nèi)存里,一旦程序結(jié)束或計算機關(guān)機,它們就會煙消云散。如果我們想把這些對象“持久化”保存下來,或者在網(wǎng)絡上傳輸,該怎么辦?
這就是**序列化(Serialization)**發(fā)揮作用的時刻。簡單來說,序列化就是把內(nèi)存中的對象轉(zhuǎn)換成可以存儲(存入文件)或傳輸(通過網(wǎng)絡發(fā)送)的字節(jié)流的過程。而反序列化,則是把字節(jié)流重新變回對象。
Python 標準庫中有一個非常強大但也常被誤解的模塊——pickle。
很多人把 Pickle 簡單地理解為“打包”,這其實并不準確。雖然它確實能把多個對象“打包”成一個字節(jié)流,但它真正的核心能力在于幾乎能序列化任何 Python 對象。
Pickle 的超能力:它到底能做什么
與其他通用的序列化格式(如 JSON 或 XML)相比,Pickle 最顯著的特點是通用性。
- JSON 只能處理基本類型(字符串、數(shù)字、列表、字典等)。如果你想把一個自定義的
User類實例存成 JSON,你必須先手動把它轉(zhuǎn)換成字典。 - Pickle 則不同,它可以直接處理:
- 復雜的內(nèi)置類型:集合(set)、復數(shù)(complex)、元組(tuple)等。
- 自定義類和實例:它會自動記錄類的定義和實例的狀態(tài)。
- 函數(shù)和類:甚至可以序列化函數(shù)引用(雖然這有局限性,詳見后文)。
- 循環(huán)引用:如果對象 A 引用了對象 B,對象 B 又引用了對象 A,Pickle 能正確處理這種循環(huán),而不會死循環(huán)。
簡單上手:Pickle 的基本用法
Pickle 的 API 非常直觀,核心函數(shù)主要有四個:dump(轉(zhuǎn)儲)、dumps(轉(zhuǎn)儲為字符串)、load(加載)、loads(從字符串加載)。
import pickle
# 定義一個稍微復雜的對象
class Player:
def __init__(self, name, level):
self.name = name
self.level = level
def upgrade(self):
self.level += 1
# 創(chuàng)建實例
p1 = Player("Alice", 10)
data = {"players": [p1], "meta": "game_save_v1"}
# 1. 序列化到文件 (dump)
with open("save.pkl", "wb") as f: # 注意:必須以二進制寫模式 'wb' 打開
pickle.dump(data, f)
# 2. 從文件反序列化 (load)
with open("save.pkl", "rb") as f: # 注意:必須以二進制讀模式 'rb' 打開
loaded_data = pickle.load(f)
print(f"加載后的玩家: {loaded_data['players'][0].name}, 等級: {loaded_data['players'][0].level}")
# 輸出: 加載后的玩家: Alice, 等級: 10
這段代碼展示了 Pickle 的便捷性。p1 是一個自定義類的實例,通過 Pickle,它被完美地保存并復活了。
2. 深入 Pickle 的工作原理與協(xié)議版本
要真正掌握 Pickle,我們需要揭開它的面紗,看看它在“打包”的過程中到底發(fā)生了什么。
Pickle 虛擬機與協(xié)議
Pickle 并不是簡單地把對象轉(zhuǎn)成字節(jié),它其實是在執(zhí)行一個基于棧的序列化語言。當你調(diào)用 pickle.dump(obj, f) 時,Pickle 會把對象轉(zhuǎn)換成一系列指令(opcodes),這些指令描述了如何重建該對象。當反序列化時,Pickle 虛擬機讀取這些指令,壓入棧,最終構(gòu)建出對象。
這就引出了一個重要的概念:Pickle 協(xié)議(Protocol)。
Python 提供了 5 個主要的協(xié)議版本(0-5),通過 pickle.dump(obj, file, protocol=...) 指定。
- Protocol 0:舊式的文本格式,人類可讀(某種程度上),但效率低,不支持二進制數(shù)據(jù)。
- Protocol 1 & 2:舊式二進制格式,效率有所提升,但仍然比較古老。
- Protocol 3:Python 3.0 引入,是 Python 3.x 早期的默認值。引入了對字節(jié)對象的更好支持。
- Protocol 4:Python 3.4 引入,支持更大的對象(超過 4GB),更多現(xiàn)代數(shù)據(jù)類型。
- Protocol 5:Python 3.8 引入,引入了**帶外數(shù)據(jù)(Out-of-band data)**支持,允許將大塊字節(jié)數(shù)據(jù)(如 NumPy 數(shù)組)直接傳輸而不必先拷貝到 Pickle 流中,極大提升了大數(shù)據(jù)處理的效率。
實用建議:除非你需要兼容非常古老的 Python 2 環(huán)境,否則請始終使用 protocol=pickle.HIGHEST_PROTOCOL(或者至少 Protocol 4),以獲得最佳性能和功能支持。
甚至可以序列化代碼
Pickle 甚至可以序列化函數(shù)和類定義,但這依賴于引用而非包含。
def my_function():
return "Hello"
# 序列化函數(shù)本身(實際上只保存了引用路徑)
pickled_func = pickle.dumps(my_function)
# 反序列化
unpickled_func = pickle.loads(pickled_func)
# print(unpickled_func()) # 這在同一個腳本里通常能工作
這背后的邏輯是:Pickle 并沒有把函數(shù)的字節(jié)碼打包進去,而是記錄了“這個函數(shù)叫什么,在哪個模塊”。當反序列化時,它會去導入那個模塊里的同名函數(shù)。這意味著,如果你把一個 Pickle 文件發(fā)給沒有該函數(shù)定義的別人,反序列化就會失敗。
3. 安全性:Pickle 是最危險的“打包”工具
這是 Pickle 最核心、最需要警惕的部分。永遠不要 Unpickle(反序列化)來自不可信來源的數(shù)據(jù)!
為什么 Pickle 如此危險
JSON 或 XML 只是數(shù)據(jù),它們本身不會執(zhí)行任何操作。但 Pickle 不同,它是一組指令。當你反序列化時,你實際上是在執(zhí)行一段代碼。
Pickle 允許定義 __reduce__ 方法,該方法可以告訴 Pickle 在反序列化時應該執(zhí)行什么操作。
攻擊演示(概念性代碼,請勿在生產(chǎn)環(huán)境運行):
假設黑客構(gòu)造了一個惡意的 Pickle 文件,其中包含了一個惡意類:
import pickle
import os
class Malicious:
def __reduce__(self):
# 當這個對象被反序列化時,會自動執(zhí)行 os.system
return (os.system, ("echo '你的電腦被黑客控制了!'", ))
# 黑客生成惡意文件
with open("evil.pkl", "wb") as f:
pickle.dump(Malicious(), f)
如果你不小心在你的服務器上加載了這個 evil.pkl 文件:
# 受害者代碼
with open("evil.pkl", "rb") as f:
data = pickle.load(f) # 危險!此時代碼已經(jīng)執(zhí)行!
結(jié)果就是 os.system("echo '...'") 被執(zhí)行。這不僅僅是執(zhí)行代碼,黑客可以通過類似方式執(zhí)行任何系統(tǒng)命令,比如刪除文件、下載惡意軟件、發(fā)起網(wǎng)絡攻擊等。
如何防范
- 絕對不要反序列化不可信數(shù)據(jù):這是鐵律。如果你必須接收用戶上傳的數(shù)據(jù),不要用 Pickle。
- 使用更安全的替代品:
- JSON:最通用,安全。
- MessagePack:二進制版的 JSON,高效且相對安全。
- Protobuf / FlatBuffers:Google 的方案,適合高性能、強類型場景。
- 如果必須用 Pickle:確保數(shù)據(jù)來源經(jīng)過了嚴格的驗證(例如,通過了身份驗證的內(nèi)部系統(tǒng)之間傳輸),或者使用 HMAC 等簽名機制驗證數(shù)據(jù)的完整性。
4. Pickle 的局限性與調(diào)試技巧
雖然 Pickle 很強大,但它并非萬能。在實際開發(fā)中,我們經(jīng)常會遇到一些“坑”。
4.1 無法序列化的對象
有些對象是無法被 Pickle 的,通常會拋出 TypeError。
- 文件句柄/網(wǎng)絡連接:打開的文件或 Socket 連接不能序列化,因為它們代表操作系統(tǒng)資源,序列化它們沒有意義,重啟后也無法恢復連接。
- Lambda 函數(shù) / 匿名函數(shù):Pickle 無法序列化通過 lambda 表達式定義的函數(shù),因為它沒有名字,無法通過“引用路徑”找到。
- 生成器(Generators):生成器函數(shù)本身可以序列化,但生成器對象(處于運行狀態(tài)的)通常不能,因為它們包含復雜的執(zhí)行狀態(tài)。
- 外部資源:如數(shù)據(jù)庫連接池等。
解決方案:在自定義類中實現(xiàn) __getstate__ 和 __setstate__ 方法。
class ResourceHolder:
def __init__(self):
self.db_connection = "模擬連接對象"
self.config = {"key": "value"}
def __getstate__(self):
# 序列化前調(diào)用:返回需要保存的字典
state = self.__dict__.copy()
# 移除不能序列化的部分
del state['db_connection']
return state
def __setstate__(self, state):
# 反序列化后調(diào)用:恢復對象狀態(tài)
self.__dict__.update(state)
# 重新初始化連接
self.db_connection = "重新建立的連接"
4.2 版本兼容性噩夢
這是 Pickle 最令人頭疼的問題之一。
如果你的類定義發(fā)生了變化,比如:
- 修改了類名。
- 修改了模塊名(文件移動)。
- 增加或刪除了
__init__中的參數(shù)。
那么反序列化舊的 Pickle 文件很可能會失敗,或者產(chǎn)生意想不到的結(jié)果。
調(diào)試技巧:
- 保持類定義穩(wěn)定:盡量不要隨意改動已用于持久化存儲的類的結(jié)構(gòu)。
- 使用版本號:在 Pickle 數(shù)據(jù)中包含一個版本字段,或者使用
__setstate__來處理舊版本數(shù)據(jù)的兼容邏輯。 - 使用
__reduce__自定義重建邏輯:如果類結(jié)構(gòu)大改,可以通過__reduce__指定如何用新類重建舊數(shù)據(jù)。
4.3 性能問題
雖然 Pickle 速度很快,但在處理海量小對象時,開銷依然可觀。對于科學計算(如 NumPy 數(shù)組),標準 Pickle 效率較低。
解決方案:
- 使用
pickle.HIGHEST_PROTOCOL。 - 對于 NumPy 數(shù)組,使用
pickle.dumps時,如果協(xié)議版本足夠高,它會嘗試使用 NumPy 自己的序列化邏輯,效率會高很多?;蛘咧苯邮褂?NumPy 自帶的.npy格式。
5. 總結(jié)與最佳實踐
Pickle 是 Python 生態(tài)中一把鋒利的瑞士軍刀。它讓對象的持久化變得極其簡單,能夠處理復雜的 Python 數(shù)據(jù)結(jié)構(gòu)。
核心觀點總結(jié):
- 便利與風險并存:Pickle 的“能序列化一切”特性正是其最大的安全漏洞。不要反序列化不可信來源的數(shù)據(jù)。
- 協(xié)議很重要:始終使用最新的協(xié)議版本(如 Protocol 4 或 5)以獲得更好的性能和功能。
- 處理變動:如果數(shù)據(jù)需要長期保存,請謹慎對待類定義的變更,利用
__getstate__/__setstate__做好兼容性管理。 - 場景選擇:
- 適合:臨時緩存、內(nèi)部系統(tǒng)間通信、保存復雜的 Python 內(nèi)部狀態(tài)(如 Scikit-learn 模型)。
- 不適合:用戶數(shù)據(jù)交換、需要跨語言交互的場景(此時請用 JSON、Protobuf)、長期歸檔存儲(因為類定義可能隨代碼迭代而失效)。
到此這篇關(guān)于Python使用Pickle模塊序列化對象的完整指南的文章就介紹到這了,更多相關(guān)Python Pickle序列化對象內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
python可視化 matplotlib畫圖使用colorbar工具自定義顏色
這篇文章主要介紹了python可視化 matplotlib畫圖使用colorbar工具自定義顏色,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-12-12
opencv+pyQt5實現(xiàn)圖片閾值編輯器/尋色塊閾值利器
這篇文章主要介紹了opencv+pyQt5實現(xiàn)圖片閾值編輯器/尋色塊閾值利器,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-11-11
pyqt5利用pyqtDesigner實現(xiàn)登錄界面
這篇文章主要為大家詳細介紹了pyqt5利用pyqtDesigner實現(xiàn)登錄界面,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-03-03
python之Flask實現(xiàn)簡單登錄功能的示例代碼
這篇文章主要介紹了python之Flask實現(xiàn)簡單登錄功能的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2018-12-12

