pickle --- Python 物件序列化¶
原始碼:Lib/pickle.py
pickle 模組實作的是一個在二進位層級上對 Python 物件進行序列化(serialize)或去序列化(de-serialize)。"Pickling" 用於專門指摘將一個 Python 物件轉換為一個二進位串流的過程,"unpickling" 則相反,指的是將一個(來自 binary file 或 bytes-like object 的)二進位串流轉換回 Python 物件的過程。Pickling(和 unpickling)的過程也可能被稱作 "serialization", "marshalling," [1] 或 "flattening"。不過,為了避免混淆,本文件將統一稱作封裝(pickling)、拆封(unpickling)。
警告
pickle 模組並不安全,切記只拆封你信任的資料。
pickle 封包是有可能被建立來在拆封的時候執行任意惡意程式碼的。絕對不要拆封任何你無法信任其來源、或可能被修改過的 pickle 封包。
建議你可以使用 hmac 模組來簽署這個封包,以確保其未被修改過。
如果你在處理不受信任的資料,其他比較安全的序列化格式(例如 json)可能會更適合。請參照 See 和 json 的比較 的說明。
和其他 Python 模組的關係¶
和 marshal 的比較¶
Python 有另一個比較原始的序列化模組叫 marshal,不過其設計目的是為了支援 Python 的預編譯功能 .pyc 的運作。總地來說,請盡可能地使用 pickle,沒事不要用 marshal。
pickle 和 marshal 有幾個明顯不同的地方:
和 json 的比較¶
pickle 協定和 JSON (JavaScript Object Notation) 有一些根本上的不同:
JSON 以文字形式作為序列化的輸出(輸出 unicode 文字,但大多數又會被編碼為
UTF-8),而 pickle 則是以二進位形式作為序列化的輸出;JSON 是人類可讀的,而 pickle 則無法;
JSON 具有高互通性(interoperability)且在 Python 以外的環境也被大量利用,但 pickle 只能在 Python 內使用。
預設狀態下的 JSON 只能紀錄一小部份的 Python 內建型別,且無法紀錄自訂類別;但透過 Python 的自省措施,pickle 可以紀錄絕大多數的 Python 型別(其他比較複雜的狀況也可以透過實作 specific object APIs 來解決);
去序列化不安全的 JSON 不會產生任意程式執行的風險,但去序列化不安全的 pickle 會。
也參考
json module: 是標準函式庫的一部分,可讓使用者進行 JSON 的序列化與去序列化。
資料串流格式¶
pickle 使用的資料格式是針對 Python 而設計的。好處是他不會受到外部標準(像是 JSON,無法紀錄指標共用)的限制;不過這也代表其他不是 Python 的程式可能無法重建 pickle 封裝的 Python 物件。
以預設設定來說,pickle 使用相對緊湊的二進位形式來儲存資料。如果你需要盡可能地縮小檔案大小,你可以壓縮封裝的資料。
pickletools 含有工具可分析 pickle 所產生的資料流。pickletools 的源始碼詳細地記載了所有 pickle 協定的操作碼(opcode)。
截至目前為止,共有六種不同版本的協定可用於封裝 pickle。數字越大版本代表你需要使用越新的 Python 版本來拆封相應的 pickle 封裝。
版本 0 的協定是最初「人類可讀」的版本,且可以向前支援早期版本的 Python。
版本 1 的協定使用舊的二進位格式,一樣能向前支援早期版本的 Python。
版本 2 的協定在 Python 2.3 中初次被引入。其可提供更高效率的 new-style classes 封裝過程。請參閱 PEP 307 以了解版本 2 帶來的改進。
版本 3 的協定在 Python 3.0 被新增。現在能支援封裝
bytes的物件且無法被 2.x 版本的 Python 拆封。在 3.0~3.7 的 Python 預設使用 3 版協定。版本 4 的協定在 Python 3.4 被新增。現在能支援超大物件的封裝、更多種型別的物件以及針對部份資料格式的儲存進行最佳化。從 Python 3.8 到 3.13,預設使用第 4 版協定。請參閱 PEP 3154 以了解第 4 版協定改進的細節。
版本 5 的協定在 Python 3.8 被新增。現在能支援帶外資料(Out-of-band data)並加速帶內資料的處理速度。自 3.14 起這是預設使用的協定。請參閱 PEP 574 以了解第 5 版協定改進的細節。
備註
資料序列化是一個比資料持久化更早出現的概念;雖然 pickle 可以讀寫檔案物件,但它並不處理命名持久物件的問題,也不處理對持久物件的並行存取、一個更棘手的問題。pickle 模組可以將複雜物件轉換成位元組串流,也可以將位元組串流轉換回具有相同原始內部結構的物件。對這些位元組串流最常見的處理方式是將它們寫入檔案中,但也可以將它們透過網路傳送或儲存在一個資料庫中。shelve 模組提供了一個簡單的介面來讓使用者在 DBM 風格的資料庫檔案中對物件進行封裝和拆封的操作。
模組介面¶
想要序列化一個物件,你只需要呼叫 dumps() 函式。而當你想要去序列化一個資料流時,你只需要呼叫 loads() 即可。不過,若你希望能各自對序列化和去序列化的過程中有更多的掌控度,你可以自訂一個 Pickler 或 Unpickler 物件。
pickle 模組提供以下常數:
- pickle.DEFAULT_PROTOCOL¶
一個整數,用於 pickle 的預設 協定版本。可能小於
HIGHEST_PROTOCOL。目前預設協定是 5,在 Python 3.8 中引入,與之前的版本不相容。此版本引入了對 out-of-band buffer 的支援,其中 PEP 3118 相容的資料可以與主要 pickle 串流分開傳輸。在 3.0 版的變更: 預設協定版本為 3。
在 3.8 版的變更: 預設協定版本為 4。
在 3.14 版的變更: 預設協定是 5。
pickle 模組提供下列函式來簡化封裝的過程:
- pickle.dump(obj, file, protocol=None, *, fix_imports=True, buffer_callback=None)¶
將被封裝成 pickle 形式的物件 obj 寫入到已開啟的file object file。這等效於
Pickler(file, protocol).dump(obj)。引數 file、protocol、fix_imports 和 buffer_callback 的意義與
Pickler建構式中的相同。在 3.8 版的變更: 新增 buffer_callback 引數。
- pickle.dumps(obj, protocol=None, *, fix_imports=True, buffer_callback=None)¶
將被封裝為 pickle 形式的物件 obj 以
bytes類別回傳,而非寫入進檔案。引數 protocol、fix_imports 和 buffer_callback 的意義和
Pickler建構式中的相同。在 3.8 版的變更: 新增 buffer_callback 引數。
- pickle.load(file, *, fix_imports=True, encoding='ASCII', errors='strict', buffers=None)¶
從已開啟的 檔案物件 file 中讀取已序列化的物件,並傳回其重建後的物件階層。這相當於呼叫
Unpickler(file).load()。模組會自動偵測 pickle 封包所使用的協定版本,所以無須另外指定。超出 pickle 封包表示範圍的位元組將被忽略。
引數 file、fix_imports、encoding、errors、strict 和 buffers 的意義和
Unpickler建構式中的相同。在 3.8 版的變更: 新增 buffer 引數。
- pickle.loads(data, /, *, fix_imports=True, encoding='ASCII', errors='strict', buffers=None)¶
回傳從 data 的 pickle 封包重建後的物件階層。data 必須是一個 bytes-like object。
模組會自動偵測 pickle 封包所使用的協定版本,所以無須另外指定。超出 pickle 封包表示範圍的位元組將被忽略。
引數 fix_imports、encoding、errors、strict 和 buffers 的意義與
Unpickler建構式所用的相同。在 3.8 版的變更: 新增 buffer 引數。
pickle 模組定義了以下三種例外:
- exception pickle.PicklingError¶
當
Pickler遭遇無法封裝物件時會引發的例外。繼承PickleError類別。請參閱 哪些物件能或不能被封裝、拆封? 以了解哪些物件是可以被封裝的。
- exception pickle.UnpicklingError¶
拆封物件時遇到問題(如資料毀損或違反安全性原則等)所引發的意外。繼承自
PickleError類別。拆封的時候還是可能會遭遇其他不在此列的例外(例如:AttributeError、EOFError、ImportError、或 IndexError),請注意。
引入模組 pickle 時會帶來三個類別:Pickler、Unpickler 和 PickleBuffer:
- class pickle.Pickler(file, protocol=None, *, fix_imports=True, buffer_callback=None)¶
接受一個用以寫入 pickle 資料流的二進位檔案。
可選引數 protocol 接受整數,用來要求封裝器(pickler)使用指定的協定;支援從 0 版起到
HIGHEST_PROTOCOL版的協定。如未指定,則預設為DEFAULT_PROTOCOL。若指定了負數,則視為選擇HIGHEST_PROTOCOL。引數 file 必須支援可寫入單一位元組引數的 write() 方法。只要滿足此條件,傳入的物件可以是一個硬碟上二進位檔案、一個
io.BytesIO實例或任何其他滿足這個介面要求的物件。若 fix_imports 設為 true 且 protocol 版本小於 3,本模組會嘗試將 Python 3 的新模組名稱轉換為 Python 2 所支援的舊名,以讓 Python 2 能正確地讀取此資料流。
如果 buffer_callback 是
None(預設值),緩衝區的視圖會作為 pickle 封裝串流的一部分被序列化進 file 中。如果 buffer_callback 不是
None,則它可以被多次呼叫並回傳一個緩衝區的視圖。如果回呼函式回傳一個假值(例如None),則所給的緩衝區將被視為 帶外資料;否則,該緩衝區將被視為 pickle 串流的帶內資料被序列化。如果 buffer_callback 不是
None且 protocol 是None或小於 5 則會報錯。在 3.8 版的變更: 新增 buffer_callback 引數。
- dump(obj)¶
將已封裝(pickled)的 obj 寫入已在建構式中開啟的對應檔案。
- persistent_id(obj)¶
預設不進行任何動作。這是一種抽象方法,用於讓後續繼承這個類別的物件可以覆寫本方法函式。
如果
persistent_id()回傳None,則 obj 會照一般的方式進行封裝(pickling)。若回傳其他值,則Pickler會將該值作為 obj 的永久識別碼回傳。此永久識別碼的意義應由Unpickler.persistent_load()定義。請注意persistent_id()回傳的值本身不能擁有自己的永久識別碼。關於細節與用法範例請見 外部物件持久化。
在 3.13 版的變更: 在 C 的
Pickler實作中的增加了這個方法的預設實作。
- dispatch_table¶
封裝器(pickler)物件含有的的調度表是一個縮減函式(reduction function)的註冊表,可以使用
copyreg.pickle()來宣告這類縮減函式。它是一個以類別為鍵、還原函式為值的映射表。縮減函式應準備接收一個對應類別的引數,並應遵循與__reduce__()方法相同的介面。預設情況下,封裝器(pickler)物件不會有
dispatch_table屬性,而是會使用由copyreg模組管理的全域調度表。不過,若要自訂某個封裝器(pickler)物件的序列化行為,可以將dispatch_table屬性設置為類字典物件。另外,如果Pickler的子類別具有dispatch_table屬性,那麼這個屬性將作為該子類別實例的預設調度表。關於用法範例請見 調度表。
在 3.3 版被加入.
- reducer_override(obj)¶
一個可以在
Pickler子類別中被定義的縮減器(reducer)。這個方法的優先度高於任何其他分派表中的縮減器。他應該要有和__reduce__()方法相同的函式介面,且可以可選地回傳NotImplemented以後備(fallback)使用分派表中登錄的縮減方法來封裝obj。請查閱 針對型別、函式或特定物件定製縮減函式 來參考其他較詳細的範例。
在 3.8 版被加入.
- fast¶
已棄用。如果設置為 true,將啟用快速模式。快速模式會停用備忘(memo),因此能透過不產生多餘的 PUT 操作碼(OpCode)來加速封裝過程。它不應被用於自我參照物件,否則將導致
Pickler陷入無限遞迴。使用
pickletools.optimize()以獲得更緊湊的 pickle 輸出。
- clear_memo()¶
清除 pickler 的「memo」。
memo 是一種資料結構,用來記錄 pickler 已經處理過的物件,以便共享或遞迴的物件能以參照而非值的方式進行 pickle。此方法在重複使用 pickler 時很有用。
- class pickle.Unpickler(file, *, fix_imports=True, encoding='ASCII', errors='strict', buffers=None)¶
這個物件接受一個二進位檔案 file 來從中讀取 pickle 資料流。
協定版本號會被自動偵測,所以不需要在這邊手動輸入。
參數 file 必須擁有三個方法,分別是接受整數作為引數的 read() 方法、接受緩衝區作為引數的 readinto() 方法以及不需要引數的 readline() 方法,如同在
io.BufferedIOBase的介面一樣。因此,file 可以是一個以二進位讀取模式開啟的檔案、一個io.BytesIO物件、或任何符合此介面的自訂物件。可選引數 fix_imports、encoding 和 errors 用來控制 Python 2 pickle 資料的相容性支援。如果 fix_imports 為 true,則 pickle 模組會嘗試將舊的 Python 2 模組名稱映射到 Python 3 中使用的新名稱。encoding 和 errors 告訴 pickle 模組如何解碼由 Python 2 pickle 封裝的 8 位元字串實例;encoding 和 errors 預設分別為 'ASCII' 和 'strict'。encoding 可以設定為 'bytes' 以將這些 8 位元字串實例讀為位元組物件。而由 Python 2 封裝的 NumPy 陣列、
datetime、date和time的實例則必須使用encoding='latin1'來拆封。如果 buffers 是
None(預設值),那麼去序列化所需的所有資料都必須已經包含在 pickle 串流中。這意味著當初在建立對應的Pickler時(或在呼叫dump()或dumps()時)*buffer_callback* 引數必須為None。如果 buffers 不是
None,則其應該是一個可疊代物件,內含數個支援緩衝區的物件,並且每當 pickle 串流引用一個帶外緩衝區視圖時將會被照順序消耗。這些緩衝資料當初建立時應已按照順序給定於 Pickler 物件中的 buffer_callback。在 3.8 版的變更: 新增 buffer 引數。
- load()¶
開啟先前被傳入建構子的檔案,從中讀取一個被 pickle 封裝的物件,並回傳重建完成的物件階層。超過 pickle 表示範圍的位元組會被忽略。
- persistent_load(pid)¶
預設會引發
UnpicklingError例外。若有定義,則
persistent_load()將回傳符合持久化識別碼 pid 的物件。如果遭遇了無效的持久化識別碼,則會引發UnpicklingError。關於細節與用法範例請見 外部物件持久化。
在 3.13 版的變更: 在 C 的
Unpickler實作中的增加了這個方法的預設實作。
- find_class(module, name)¶
如有需要將引入 module,並從中回傳名為 name 的物件,這裡的 module 和 name 引數接受的輸入是
str物件。注意,雖然名稱上看起來不像,但find_class()亦可被用於尋找其他函式。子類別可以覆寫此方法以控制可以載入哪些類型的物件、以及如何載入它們,從而潛在地降低安全性風險。詳情請參考限制全域物件。
引發一個附帶引數
module、name的稽核事件pickle.find_class。
- class pickle.PickleBuffer(buffer)¶
一個表示了含有可封裝資料緩衝區的包裝函式(wrapper function)。buffer 必須是一個 提供緩衝區 的物件,例如一個 類位元組物件 或 N 維陣列。
PickleBuffer本身就是一個提供緩衝區的物件,所以是能夠將其提供給其它「預期收到含有緩衝物件的 API」的,比如memoryview。PickleBuffer物件僅能由 5 版或以上的 pickle 協定進行封裝。該物件亦能被作為帶外資料來進行帶外資料序列化在 3.8 版被加入.
- raw()¶
回傳此緩衝區底層記憶體區域的
memoryview。被回傳的物件是一個(在 C 語言的 formatter 格式中)以B(unsigned bytes) 二進位格式儲存、一維且列連續(C-contiguous)的 memoryview。如果緩衝區既不是列連續(C-contiguous)也不是行連續(Fortran-contiguous)的,則會引發BufferError。
- release()¶
釋放 PickleBuffer 物件現正曝光中的緩衝區。
哪些物件能或不能被封裝、拆封?¶
下列型別可以被封裝:
內建常數(
None、True、False、Ellipsis和NotImplemented);整數、浮點數和複數;
字串、位元組物件、位元組陣列;
元組(tuple)、串列(list)、集合(set)和僅含有可封裝物件的字典;
在模組最表面的層級就能被存取的類別;
實例,只要在呼叫了
__getstate__()後其回傳值全都是可封裝物件。(詳情請參閱 Pickling 類別實例)。
嘗試封裝無法封裝的物件會引發 PicklingError 例外;注意當這種情況發生時,可能已經有未知數量的位元組已被寫入到檔案。嘗試封裝深度遞迴的資料結構可能會導致其超出最大遞迴深度,在這種情況下會引發 RecursionError 例外。你可以(小心地)使用 sys.setrecursionlimit() 來提高此上限。
請注意,函式(內建及自訂兩者皆是)是依據完整的 限定名稱 來封裝,而非依其值。[2] 這意味著封裝時只有函式名稱、所屬的模組和所屬的類別名稱會被封裝。函式本身的程式碼及其附帶的任何屬性均不會被封裝。因此,在拆封該物件的環境中,定義此函式的模組必須可被引入,且該模組必須包含具此命名之物件,否則將引發例外。 [3]
同樣情況,類別是依照其完整限定名稱來進行封裝,因此在進行拆封的環境中會具有同上的限制。類別中的程式碼或資料皆不會被封裝,因此在以下範例中,注意到類別屬性 attr 在拆封的環境中不會被還原:
class Foo:
attr = 'A class attribute'
picklestring = pickle.dumps(Foo)
這些限制就是可封裝的函式和類別必須被定義在模組頂層的原因。
同樣地,當類別實例被封裝時,它所屬類別具有的程式碼和資料不會被一起封裝。只有實例資料本身會被封裝。這是有意而為的,因為如此你才可以在類別中修正錯誤或新增其他方法,且於此同時仍能夠載入使用較早期版本的類別所建立的物件實例。如果你預計將有長期存在的物件、且該物件將經歷許多版本的更替,你可以在物件中存放一個版本號,以便未來能透過 __setstate__() 方法來進行適當的版本轉換。
Pickling 類別實例¶
在這一個章節,我們會講述如何封裝或拆封一個物件實例的相關機制,以方便你進行自訂。
大部分的實例不需要額外的程式碼就已經是可封裝的了。在這樣的預設狀況中,pickle 模組透過自省機制來取得類別及其實例的屬性。當類別實例被拆封時,其 __init__() 方法通常不會被呼叫。預設行為首先會建立一個未初始化的實例,然後還原紀錄中的屬性。以下程式碼的實作展示了前述行為:
def save(obj):
return (obj.__class__, obj.__dict__)
def restore(cls, attributes):
obj = cls.__new__(cls)
obj.__dict__.update(attributes)
return obj
被封裝的目標類別可以提供一個或數個下列特殊方法來改變 pickle 的預設行為:
- object.__getnewargs_ex__()¶
在第 2 版協定或更新的版本中,有實作
__getnewargs_ex__()方法的類別,可以決定在拆封時要傳遞給__new__()方法的值。該方法必須回傳一個(args, kwargs)的組合,其中 args 是一個位置引數的元組(tuple),kwargs 是一個用於建構物件的命名引數字典。這些資訊將在拆封時傳遞給__new__()方法。如果目標類別的方法
__new__()需要僅限關鍵字的參數時,你應該實作此方法。否則,為了提高相容性,建議你改為實作__getnewargs__()。