pickle --- Python 物件序列化

原始碼:Lib/pickle.py


pickle 模組實作的是一個在二進位層級上對 Python 物件進行序列化(serialize)或去序列化(de-serialize)。"Pickling" 用於專門指摘將一個 Python 物件轉換為一個二進位串流的過程,"unpickling" 則相反,指的是將一個(來自 binary filebytes-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

picklemarshal 有幾個明顯不同的地方:

  • 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 版協定。

  • 版本 4 的協定在 Python 3.4 被新增。現在能支援超大物件的封裝、更多種型別的物件以及針對部份資料格式的儲存進行最佳化。從 Python 3.8 起,預設使用第 4 版協定。請參閱 PEP 3154 以了解第 4 版協定改進的細節。

  • 版本 5 的協定在 Python 3.8 被新增。現在能支援帶外資料(Out-of-band data)並加速帶內資料的處理速度。請參閱 PEP 574 以了解第 5 版協定改進的細節。

備註

資料序列化是一個比資料持久化更早出現的概念;雖然 pickle 可以讀寫檔案物件,但它並不處理命名持久物件的問題,也不處理對持久物件的並行存取、一個更棘手的問題。pickle 模組可以將複雜物件轉換成位元組串流,也可以將位元組串流轉換回具有相同原始內部結構的物件。對這些位元組串流最常見的處理方式是將它們寫入檔案中,但也可以將它們透過網路傳送或儲存在一個資料庫中。shelve 模組提供了一個簡單的介面來讓使用者在 DBM 風格的資料庫檔案中對物件進行封裝和拆封的操作。

模組介面

想要序列化一個物件,你只需要呼叫 dumps() 函式。而當你想要去序列化一個資料流時,你只需要呼叫 loads() 即可。不過,若你希望能各自對序列化和去序列化的過程中有更多的掌控度,你可以自訂一個 PicklerUnpickler 物件。

pickle 模組提供以下常數:

pickle.HIGHEST_PROTOCOL

一個整數,表示可使用的最高協定版本。這個值可作為 protocol 的數值傳給 dump()dumps() 函式以及 Pickler 建構式。

pickle.DEFAULT_PROTOCOL

一個整數,指示用於序列化的預設協定版本。有可能小於 HIGHEST_PROTOCOL。目前的預設協定版本為 4,是在 Python 3.4 中首次引入的,且與先前版本不相容。

在 3.0 版的變更: 預設協定版本為 3。

在 3.8 版的變更: 預設協定版本為 4。

pickle 模組提供下列函式來簡化封裝的過程:

pickle.dump(obj, file, protocol=None, *, fix_imports=True, buffer_callback=None)

將被封裝成 pickle 形式的物件 obj 寫入到已開啟的file object file。這等效於Pickler(file, protocol).dump(obj)

引數 fileprotocolfix_importsbuffer_callback 的意義與 Pickler 建構式中的相同。

在 3.8 版的變更: 新增 buffer_callback 引數。

pickle.dumps(obj, protocol=None, *, fix_imports=True, buffer_callback=None)

將被封裝為 pickle 形式的物件 objbytes 類別回傳,而非寫入進檔案。

引數 protocolfix_importsbuffer_callback 的意義和 Pickler 建構式中的相同。

在 3.8 版的變更: 新增 buffer_callback 引數。

pickle.load(file, *, fix_imports=True, encoding='ASCII', errors='strict', buffers=None)

從已開啟的 檔案物件 file 中讀取已序列化的物件,並傳回其重建後的物件階層。這相當於呼叫 Unpickler(file).load()

模組會自動偵測 pickle 封包所使用的協定版本,所以無須另外指定。超出 pickle 封包表示範圍的位元組將被忽略。

引數 filefix_importsencodingerrorsstrictbuffers 的意義和 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_importsencodingerrorsstrictbuffers 的意義與 Unpickler 建構式所用的相同。

在 3.8 版的變更: 新增 buffer 引數。

pickle 模組定義了以下三種例外:

exception pickle.PickleError

繼承 Exception 類別。一個在封裝或拆封時遭遇其他例外時通用的基底類別。

exception pickle.PicklingError

Pickler 遭遇無法封裝物件時會引發的例外。繼承 PickleError 類別。

請參閱 哪些物件能或不能被封裝、拆封? 以了解哪些物件是可以被封裝的。

exception pickle.UnpicklingError

拆封物件時遇到問題(如資料毀損或違反安全性原則等)所引發的意外。繼承自 PickleError 類別。

拆封的時候還是可能會遭遇其他不在此列的例外(例如:AttributeError、EOFError、ImportError、或 IndexError),請注意。

引入模組 pickle 時會帶來三個類別:PicklerUnpicklerPickleBuffer

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_callbackNone (預設值),緩衝區的視圖會作為 pickle 封裝串流的一部分被序列化進 file 中。

如果 buffer_callback 不是 None,則它可以被多次呼叫並回傳一個緩衝區的視圖。如果回呼函式回傳一個假值(例如 None),則所給的緩衝區將被視為 帶外資料;否則,該緩衝區將被視為 pickle 串流的帶內資料被序列化。

如果 buffer_callback 不是 NoneprotocolNone 或小於 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 輸出。

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_importsencodingerrors 用來控制 Python 2 pickle 資料的相容性支援。如果 fix_imports 為 true,則 pickle 模組會嘗試將舊的 Python 2 模組名稱映射到 Python 3 中使用的新名稱。encodingerrors 告訴 pickle 模組如何解碼由 Python 2 pickle 封裝的 8 位元字串實例;encodingerrors 預設分別為 'ASCII' 和 'strict'。encoding 可以設定為 'bytes' 以將這些 8 位元字串實例讀為位元組物件。而由 Python 2 封裝的 NumPy 陣列、datetimedatetime 的實例則必須使用 encoding='latin1' 來拆封。

如果 buffersNone(預設值),那麼去序列化所需的所有資料都必須已經包含在 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 的物件,這裡的 modulename 引數接受的輸入是 str 物件。注意,雖然名稱上看起來不像,但 find_class() 亦可被用於尋找其他函式。

子類別可以覆寫此方法以控制可以載入哪些類型的物件、以及如何載入它們,從而潛在地降低安全性風險。詳情請參考限制全域物件

引發一個附帶引數 modulename稽核事件 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 物件現正曝光中的緩衝區。

哪些物件能或不能被封裝、拆封?

下列型別可以被封裝:

  • 內建常數(NoneTrueFalseEllipsisNotImplemented);

  • 整數、浮點數和複數;

  • 字串、位元組物件、位元組陣列;

  • 元組(tuple)、串列(list)、集合(set)和僅含有可封裝物件的字典;

  • 在模組最表面的層級就能被存取的函式(內建或自訂的皆可,不過僅限使用 def 定義的函式,lambda 函式不適用);

  • 在模組最表面的層級就能被存取的類別;

  • 實例,只要在呼叫了 __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__ 實例的類別,其預設狀態為 None

  • __dict__ 實例、但沒有 __slots__ 實例的類別,其預設狀態為 self.__dict__

  • __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_tablereducer_override() 都被定義了的話,reducer_override() 的優先度較高。

備註

出於效能考量,處裡以下物件可能不會呼叫 reducer_override()NoneTrueFalse,以及 intfloatbytesstrdictsetfrozensetlisttuple 的實例。

以下是一個簡單的例子,我們示範如何允許封裝和重建給定的類別::

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)

也參考

copyreg 模組

註冊擴充型別的 Pickle 介面建構子。

pickletools 模組

用於分析或處裡被封裝資料的工具。

shelve 模組

索引式資料庫;使用 pickle 實作。

copy 模組

物件的淺層或深度拷貝。

marshal 模組

內建型別的高效能序列化。

註解