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
沒有這個功能。這對遞迴物件和物件共用都有影響。遞迴物件是指包含自我參照的物件。這些情況在 marshal 模組中不會被處理,若嘗試使用 marshal 處理遞迴物件會導致 Python 直譯器崩潰。物件共用發生在序列化的物件階層中、不同位置對同一物件有多個參照時。
pickle
只會儲存這個被參照的物件一次,並確保所有其他參照指向這個主要的版本。共用的物件會保持共用,這對於可變(mutable)物件來說非常重要。marshal
無法序列化使用者自訂的類別和的實例。pickle
則可以讓使用者儲存並還原自訂的類別實例,前提是儲存時該類別的定義存在於與要被儲存的物件所在的模組中、且可以被引入(import)。marshal
序列化格式無法保證能在不同版本的 Python 之間移植。因為其主要的作用是支援.pyc
檔案的運作,Python 的實作人員會在需要時實作無法前向相容的序列化方式。但只要選擇了相容的 pickle 協定,且處理了 Python 2 和 Python 3 之間的資料類型差異,pickle
序列化協定能保證在不同 Python 版本間的相容性。
和 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 版協定。Protocol version 4 was added in Python 3.4. It adds support for very large objects, pickling more kinds of objects, and some data format optimizations. This was the default protocol in Python 3.8--3.13. Refer to PEP 3154 for information about improvements brought by protocol 4.
Protocol version 5 was added in Python 3.8. It adds support for out-of-band data and speedup for in-band data. It is the default protocol starting with Python 3.14. Refer to PEP 574 for information about improvements brought by protocol 5.
模組介面¶
想要序列化一個物件,你只需要呼叫 dumps()
函式。而當你想要去序列化一個資料流時,你只需要呼叫 loads()
即可。不過,若你希望能各自對序列化和去序列化的過程中有更多的掌控度,你可以自訂一個 Pickler
或 Unpickler
物件。
pickle
模組提供以下常數:
- pickle.DEFAULT_PROTOCOL¶
An integer, the default protocol version used for pickling. May be less than
HIGHEST_PROTOCOL
. Currently the default protocol is 5, introduced in Python 3.8 and incompatible with previous versions. This version introduces support for out-of-band buffers, where PEP 3118-compatible data can be transmitted separately from the main pickle stream.在 3.0 版的變更: 預設協定版本為 3。
在 3.8 版的變更: 預設協定版本為 4。
在 3.14 版的變更: The default protocol is 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 的 class:!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 輸出。
- 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 版的變更: Add the default implementation of this method in the C implementation of
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__()
。在 3.6 版的變更: 在第 2、3 版的協定中現在改為使用
__getnewargs_ex__()
。
- object.__getnewargs__()¶
此方法與
__getnewargs_ex__()
的目的一樣,但僅支援位置參數。它必須回傳一個由傳入引數所組成的元組(tuple)``args``,這些引數會在拆封時傳遞給__new__()
方法。當有定義
__getnewargs_ex__()
的時候便不會呼叫__getnewargs__()
。在 3.6 版的變更: 在 Python 3.6 之前、版本 2 和版本 3 的協定中,會呼叫
__getnewargs__()
而非__getnewargs_ex__()
。
- object.__getstate__()¶
目標類別可以透過覆寫方法
__getstate__()
進一步影響其實例被封裝的方式。封裝時,呼叫該方法所返回的物件將作為該實例的內容被封裝、而非一個預設狀態。以下列出幾種預設狀態:有
__dict__
和__slots__
實例的類別,其預設狀態是一個含有兩個字典的元組(tuple),該二字典分別為self.__dict__
本身,和紀錄欄位(slot)名稱和值對應關係的字典(只有含有值的欄位(slot)會被紀錄其中)。沒有
__dict__
但有__slots__
實例的類別,其預設狀態是一個二元組(tuple),元組中的第一個值是None
,第二個值則是紀錄欄位(slot)名稱和值對應關係的字典(與前一項提到的字典是同一個)。
在 3.11 版的變更: 在
object
類別中增加預設的__getstate__()
實作。
- object.__setstate__(state)¶
在拆封時,如果類別定義了
__setstate__()
,則會使用拆封後的狀態呼叫它。在這種情況下,紀錄狀態的物件不需要是字典(dictionary)。否則,封裝時的狀態紀錄必須是一個字典,其紀錄的項目將被賦值給新實例的字典。備註
如果
__reduce__()
在封裝時返回了None
狀態,則拆封時就不會去呼叫__setstate__()
。
參閱 處裡紀錄大量狀態的物件 以了解 __getstate__()
和 __setstate__()
的使用方法。
備註
在拆封時,某些方法如 __getattr__()
、__getattribute__()
或 __setattr__()
可能會在建立實例時被呼叫。如果這些方法依賴了某些實例內部的不變性,則應實作 __new__()
以建立此不變性,因為在拆封實例時不會呼叫 __init__()
。
如稍後所演示,pickle 並不直接使用上述方法。這些方法實際上是實作了 __reduce__()
特殊方法的拷貝協定(copy protocol)。拷貝協定提供了統一的介面,以檢索進行封裝及複製物件時所需的資料。 [4]
直接在類別中實作 __reduce__()
雖然功能強大但卻容易導致出錯。因此,設計類別者應盡可能使用高階介面(例如,__getnewargs_ex__()
、__getstate__()
和 __setstate__()
)。不過,我們也將展示一些特例狀況,在這些狀況中,使用 __reduce__()
可能是唯一的選擇、是更有效率的封裝方法或二者兼備。
- object.__reduce__()¶
目前的介面定義如下。
__reduce__()
方法不接受引數,且應回傳一個字串或一個元組(元組一般而言是較佳的選擇;所回傳的物件通常稱為「縮減值」)。如果回傳的是字串,該字串應被解讀為一個全域變數的名稱。它應是該物件相對其所在模組的本地名稱;pickle 模組會在模組命名空間中尋找,以確定該物件所在的模組。這種行為通常對於單例物件特別有用。
當返回一個元組時,其長度必須介於兩至六項元素之間。可選項可以被省略,或者其值可以被設為
None
。各項物件的語意依序為:一個將會被呼叫來創建初始版本物件的可呼叫物件。
一個用於傳遞引數給前述物件的元組。如果前述物件不接受引數輸入,則你仍應在這裡給定一個空元組。
可選項。物件狀態。如前所述,會被傳遞給該物件的
__setstate__()
方法。如果該物件沒有實作此方法,則本值必須是一個字典,且其將會被新增到物件的__dict__
屬性中。可選項。一個用來提供連續項目的疊代器(而非序列)。這些項目將個別透過
obj.append(item)
方法或成批次地透過obj.extend(list_of_items)
方法被附加到物件中。主要用於串列(list)子類別,但只要其他類別具有相應的 append 和 extend 方法以及相同的函式簽章(signature)就也可以使用。 (是否會調用append()
或extend()
方法將取決於所選用的 pickle 協定版本以及要附加的項目數量,因此必須同時支援這兩種方法。)可選項。一個產生連續鍵值對的疊代器(不是序列)。這些項目將以
obj[key] = value
方式被儲存到物件中。主要用於字典(dictionary)子類別,但只要有實現了__setitem__()
的其他類別也可以使用。可選項。一個具有
(obj, state)
函式簽章(signature)的可呼叫物件。該物件允許使用者以可編寫的邏輯,而不是物件obj
預設的__setstate__()
靜態方法去控制特定物件的狀態更新方式。如果這個物件不是None
,這個物件的呼叫優先權將優於物件obj
的__setstate__()
。在 3.8 版被加入: 加入第六個可選項(一個
(obj, state)
元組)。
- object.__reduce_ex__(protocol)¶
另外,你也可以定義一個
__reduce_ex__()
方法。唯一的不同的地方是此方法只接受協定版本(整數)作為參數。當有定義本方法時,pickle 會優先調用它而不是__reduce__()
。此外,呼叫__reduce__()
時也會自動變成呼叫這個變體版本。此方法主要是為了向後相容的舊的 Python 版本而存在。
外部物件持久化¶
為了方便物件持久化,pickle
模組支援對被封裝資料串流以外的物件參照。被參照的物件是透過一個持久化 ID 來參照的,這個 ID 應該要是字母數字字元(alphanumeric)組成的字串(協定 0) [5] 或者是任意的物件(任何較新的協定)。
pickle
沒有定義要如何解決或分派這個持久化 ID 的問題;故其處理方式有賴使用者自行定義在封裝器(pickler)以及拆封器(unpickler)中。方法的名稱各自為 persistent_id()
和 persistent_load()
。
要封裝具有外部持久化 ID 的物件,封裝器(pickler)必須擁有一個自訂的方法 persistent_id()
,這個方法將接收一個物件作為參數,並回傳 None 或該物件的持久化 ID。當回傳 None 時,封裝器會正常地封裝該物件。當回傳一個持久化 ID 字串時,封裝器會封裝該物件並加上一個標記,讓拆封器(unpikler)能識別它是一個持久化 ID。
要拆封外部物件,拆封器(unpickler)必須有一個自訂的 persistent_load()
方法,該方法應接受一個持久化 ID 物件,並回傳相對應的物件。
以下是一個完整的範例,用以說明如何使用持久化 ID 來封裝具外部參照的物件。
# 展示如何使用持久化 ID 來封裝外部物件的簡單範例
import pickle
import sqlite3
from collections import namedtuple
# 代表資料庫中紀錄的一個簡易類別
MemoRecord = namedtuple("MemoRecord", "key, task")
class DBPickler(pickle.Pickler):
def persistent_id(self, obj):
# 我們派發出一個持久 ID,而不是像一般類別實例那樣封裝 MemoRecord。
if isinstance(obj, MemoRecord):
# 我們的持久 ID 就是一個元組,裡面包含一個標籤和一個鍵,指向資料庫中的特定紀錄。
return ("MemoRecord", obj.key)
else:
# 如果 obj 沒有持久 ID,則返回 None。這表示 obj 像平常那樣封裝即可。
return None
class DBUnpickler(pickle.Unpickler):
def __init__(self, file, connection):
super().__init__(file)
self.connection = connection
def persistent_load(self, pid):
# 每當遇到持久 ID 時,此方法都會被呼叫。
# pid 是 DBPickler 所回傳的元組。
cursor = self.connection.cursor()
type_tag, key_id = pid
if type_tag == "MemoRecord":
# 從資料庫中抓取所引用的紀錄並返回。
cursor.execute("SELECT * FROM memos WHERE key=?", (str(key_id),))
key, task = cursor.fetchone()
return MemoRecord(key, task)
else:
# 如果無法返回正確的物件,則必須拋出錯誤。
# 否則 unpickler 會誤認為 None 是持久 ID 所引用的物件。
raise pickle.UnpicklingError("unsupported persistent object")
def main():
import io
import pprint
# 初始化資料庫。
conn = sqlite3.connect(":memory:")
cursor = conn.cursor()
cursor.execute("CREATE TABLE memos(key INTEGER PRIMARY KEY, task TEXT)")
tasks = (
'give food to fish',
'prepare group meeting',
'fight with a zebra',
)
for task in tasks:
cursor.execute("INSERT INTO memos VALUES(NULL, ?)", (task,))
# 抓取要封裝的紀錄。
cursor.execute("SELECT * FROM memos")
memos = [MemoRecord(key, task) for key, task in cursor]
# 使用我們自訂的 DBPickler 來保存紀錄。
file = io.BytesIO()
DBPickler(file).dump(memos)
print("被封裝的紀錄:")
pprint.pprint(memos)
# 更新一筆紀錄(測試用)。
cursor.execute("UPDATE memos SET task='learn italian' WHERE key=1")
# 從 pickle 資料流中載入紀錄。
file.seek(0)
memos = DBUnpickler(file, conn).load()
print("已拆封的紀錄:")
pprint.pprint(memos)
if __name__ == '__main__':
main()
調度表¶
如果你希望在不干擾其他物件正常封裝的前提下建立一個針對特定物件的封裝器,你可建立一個有私密調度表的封裝器。
由 copyreg
模組管理的全域調度表可以 copyreg.dispatch_table
呼叫。你可以透過這個方式來基於原始 copyreg.dispatch_table
創建一個修改過的版本,作為你的專屬用途的調度表。
舉例來說:
f = io.BytesIO()
p = pickle.Pickler(f)
p.dispatch_table = copyreg.dispatch_table.copy()
p.dispatch_table[SomeClass] = reduce_SomeClass
建立了一個 pickle.Pickler
,其中含有專門處裡 SomeClass
類別的專屬調度表。此外,你也可以寫作::
class MyPickler(pickle.Pickler):
dispatch_table = copyreg.dispatch_table.copy()
dispatch_table[SomeClass] = reduce_SomeClass
f = io.BytesIO()
p = MyPickler(f)
這樣可產生相似的結果,唯一不同的是往後所有 MyPickler
預設都會使用這個專屬調度表。最後,如果將程式寫為::
copyreg.pickle(SomeClass, reduce_SomeClass)
f = io.BytesIO()
p = pickle.Pickler(f)
則會改變 copyreg
模組內建、所有使用者共通的調度表。
處裡紀錄大量狀態的物件¶
以下的範例展示了如何修改針對特定類別封裝時的行為。下面的 TextReader
類別會開啟一個文字檔案,並在每次呼叫其 readline()
方法時返回當前行編號與該行內容。如果 TextReader
實例被封裝,所有*除了檔案物件之外*的屬性成員都會被保存。在該實例被拆封時,檔案將被重新開啟,並從上次的位置繼續讀取。這個行為的達成是透過 __setstate__()
和 __getstate__()
方法來實作的。:
class TextReader:
"""列出文字檔案中的行並對其進行編號。"""
def __init__(self, filename):
self.filename = filename
self.file = open(filename)
self.lineno = 0
def readline(self):
self.lineno += 1
line = self.file.readline()
if not line:
return None
if line.endswith('\n'):
line = line[:-1]
return "%i: %s" % (self.lineno, line)
def __getstate__(self):
# 從 self.__dict__ 中複製物件的狀態。包含了所有的實例屬性。
# 使用 dict.copy() 方法以避免修改原始狀態。
state = self.__dict__.copy()
# 移除不可封裝的項目。
del state['file']
return state
def __setstate__(self, state):
# 恢復實例屬性(即 filename 和 lineno)。
self.__dict__.update(state)
# 恢復到先前開啟了檔案的狀態。為此,我們需要重新開啟它並一直讀取到行數編號相同。
file = open(self.filename)
for _ in range(self.lineno):
file.readline()
# 存檔。
self.file = file
可以這樣實際使用::
>>> reader = TextReader("hello.txt")
>>> reader.readline()
'1: Hello world!'
>>> reader.readline()
'2: I am line number two.'
>>> new_reader = pickle.loads(pickle.dumps(reader))
>>> new_reader.readline()
'3: Goodbye!'
針對型別、函數或特定物件定製縮減函數¶
在 3.8 版被加入.
有時候,dispatch_table
的彈性空間可能不夠。尤其當我們想要使用型別以外的方式來判斷如何使用自訂封裝、或者我們想要自訂特定函式和類別的封裝方法時。
如果是這樣的話,可以繼承 Pickler
類別並實作一個 reducer_override()
方法。此方法可以回傳任意的縮減元組(參閱 __reduce__()
)、也可以回傳 NotImplemented
以回退至原始的行為。
如果 dispatch_table
和 reducer_override()
都被定義了的話,reducer_override()
的優先度較高。
備註
出於效能考量,處裡以下物件可能不會呼叫 reducer_override()
:None
、True
、False
,以及 int
、float
、bytes
、str
、dict
、set
、frozenset
、list
和 tuple
的實例。
以下是一個簡單的例子,我們示範如何允許封裝和重建給定的類別::
import io
import pickle
class MyClass:
my_attribute = 1
class MyPickler(pickle.Pickler):
def reducer_override(self, obj):
"""MyClass 的自訂縮減函數。"""
if getattr(obj, "__name__", None) == "MyClass":
return type, (obj.__name__, obj.__bases__,
{'my_attribute': obj.my_attribute})
else:
# 遭遇其他物件,則使用一般的縮減方式
return NotImplemented
f = io.BytesIO()
p = MyPickler(f)
p.dump(MyClass)
del MyClass
unpickled_class = pickle.loads(f.getvalue())
assert isinstance(unpickled_class, type)
assert unpickled_class.__name__ == "MyClass"
assert unpickled_class.my_attribute == 1
帶外(Out-of-band)資料緩衝區¶
在 3.8 版被加入.
pickle
模組會被用於用於傳輸龐大的資料。此時,將複製記憶體的次數降到最低以保持效能變得很重要。然而,pickle
模組的正常操作過程中,當它將物件的圖狀結構(graph-like structure)轉換為連續的位元組串流時,本質上就涉及將資料複製到封裝流以及從封裝流複製資料。
如果*供給者*(被傳遞物件的型別的實作)與*消費者*(資訊交換系統的實作)都支援由 pickle 協定 5 或更高版本提供的帶外傳輸功能,則可以避免此一先天限制。
供給者 API¶
要封裝的大型資料物件,則必須實作一個針對 5 版協定及以上的 __reduce_ex__()
方法,該方法應返回一個 PickleBuffer
實例來處理任何大型資料(而非返回如 bytes
物件)。
一個 PickleBuffer
物件*指示*了當下底層的緩衝區狀態適合進行帶外資料傳輸。這些物件仍然相容 pickle
模組的一般使用方式。消費者程式也可以選擇介入,指示 pickle
他們將自行處理這些緩衝區。
消費者 API¶
一個資訊交換系統可以決定要自行處裡序列化物件圖時產生的 PickleBuffer
物件。
傳送端需要傳遞一個調用緩衝區的回呼函數給 Pickler
(或 dump()
或 dumps()
函數)的 buffer_callback 引數,使每次生成 PickleBuffer
時,該物件在處理物件圖時能被呼叫。除了一個簡易標記以外,由 buffer_callback 累積的緩衝區資料不會被複製到 pickle 串流中。
接收端需要傳遞一個緩衝區物件給 Unpickler
(或 load()
或 loads()
函式)的 buffers 引數。該物件須是一個可疊代的(iterable)緩衝區(buffer)物件,其中包含傳遞給 buffer_callback 的緩衝區物件。這個可疊代物件的緩衝區順序應該與它們當初被封裝時傳遞給 buffer_callback 的順序相同。這些緩衝區將提供物件重建所需的資料,以使重建器能還原出那個當時產生了 PickleBuffer
的物件。
在傳送與接收端之間,通訊系統可以自由實作轉移帶外緩衝區資料的機制。該機制可能可以利用共用記憶體機制或根據資料類型特定的壓縮方式來最佳化執行速度。
範例¶
這一個簡單的範例展示了如何實作一個可以參與帶外緩衝區封裝的 bytearray
子類別::
class ZeroCopyByteArray(bytearray):
def __reduce_ex__(self, protocol):
if protocol >= 5:
return type(self)._reconstruct, (PickleBuffer(self),), None
else:
# PickleBuffer 在 pickle 協定 <= 4 時禁止使用。
return type(self)._reconstruct, (bytearray(self),)
@classmethod
def _reconstruct(cls, obj):
with memoryview(obj) as m:
# 取得對原始緩衝區物件的控制
obj = m.obj
if type(obj) is cls:
# 若原本的緩衝區物件是 ZeroCopyByteArray,則直接回傳。
return obj
else:
return cls(obj)
如果型別正確,重建器(_reconstruct
類別方法)會返回當時提供緩衝區的物件。這個簡易實作可以模擬一個無複製行為的重建器。
在使用端,我們可以用一般的方式封裝這些物件,當我們拆封時會得到一個原始物件的副本::
b = ZeroCopyByteArray(b"abc")
data = pickle.dumps(b, protocol=5)
new_b = pickle.loads(data)
print(b == new_b) # True
print(b is new_b) # False: 曾進行過複製運算
但如果我們傳一個 buffer_callback 並在去序列化時正確返回積累的緩衝資料,我們就能拿回原始的物件::
b = ZeroCopyByteArray(b"abc")
buffers = []
data = pickle.dumps(b, protocol=5, buffer_callback=buffers.append)
new_b = pickle.loads(data, buffers=buffers)
print(b == new_b) # True
print(b is new_b) # True: 沒有進行過複製
此範例是因為受限於 bytearray
會自行分配記憶體:您無法創建以其他物件的記憶體為基礎的 bytearray
實例。不過第三方資料型態(如 NumPy 陣列)則可能沒有這個限制,而允許在不同程序或系統之間傳輸資料時使用零拷貝封裝(或儘可能地減少拷貝次數)。
也參考
PEP 574 -- 第 5 版 Pickle 協定的帶外資料(out-of-band data)處裡
限制全域物件¶
預設情況下,拆封過程將會引入任何在 pickle 資料中找到的類別或函式。對於許多應用程式來說,這種行為是不可接受的,因為它讓拆封器能夠引入並執行任意程式碼。請參見以下 pickle 資料流在載入時的行為::
>>> import pickle
>>> pickle.loads(b"cos\nsystem\n(S'echo hello world'\ntR.")
hello world
0
在這個例子中,拆封器會引入 os.system()
函式,然後執行命令「echo hello world」。雖然這個例子是無害的,但不難想像可以這個方式輕易執行任意可能對系統造成損害的命令。
基於以上原因,您可能會希望透過自訂 Unpickler.find_class()
來控制哪些是能夠被拆封的內容。與其名稱字面意義暗示的不同,實際上每當你請求一個全域物件(例如,類別或函式)時,就會調用 Unpickler.find_class()
。因此,可以透過這個方法完全禁止全域物件或將其限制在安全的子集合。
以下是一個僅允許從 builtins
模組中載入少數安全類別的拆封器(unpickler)的例子::
import builtins
import io
import pickle
safe_builtins = {
'range',
'complex',
'set',
'frozenset',
'slice',
}
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
# 只允許幾個內建的安全類別
if module == "builtins" and name in safe_builtins:
return getattr(builtins, name)
# 完全禁止任何其他類別
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
(module, name))
def restricted_loads(s):
"""一個模擬 pickle.loads() 的輔助函數"""
return RestrictedUnpickler(io.BytesIO(s)).load()
我們剛才實作的的拆封器範例正常運作的樣子::
>>> restricted_loads(pickle.dumps([1, 2, range(15)]))
[1, 2, range(0, 15)]
>>> restricted_loads(b"cos\nsystem\n(S'echo hello world'\ntR.")
Traceback (most recent call last):
...
pickle.UnpicklingError: global 'os.system' is forbidden
>>> restricted_loads(b'cbuiltins\neval\n'
... b'(S\'getattr(__import__("os"), "system")'
... b'("echo hello world")\'\ntR.')
Traceback (most recent call last):
...
pickle.UnpicklingError: global 'builtins.eval' is forbidden
正如我們的範例所示,必須謹慎審視能被拆封的內容。因此,如果您的應用場景非常關心安全性,您可能需要考慮其他選擇,例如 xmlrpc.client
中的 marshalling API 或其他第三方解決方案。
效能¶
較近期的 pickle 協定版本(從 2 版協定開始)為多種常見功能和內建型別提供了高效率的二進位編碼。此外,pickle
模組還具備一個透明化的、以 C 語言編寫的最佳化工具。
範例¶
最簡單的使用方式,調用 dump()
和 load()
函式。:
import pickle
# 任意 pickle 支援的物件。
data = {
'a': [1, 2.0, 3+4j],
'b': ("string", b"byte string"),
'c': {None, True, False}
}
with open('data.pickle', 'wb') as f:
# 使用可用的最高協定來封裝 'data' 字典。
pickle.dump(data, f, pickle.HIGHEST_PROTOCOL)
以下範例可以讀取前述程式所封裝的 pickle 資料。:
import pickle
with open('data.pickle', 'rb') as f:
# 會自動檢測資料使用的協定版本,因此我們不需要手動指定。
data = pickle.load(f)
也參考
註解