pickle — Python 物件序列化

原始碼: Lib/pickle.py


pickle 模組實現了用於序列化和反序列化 Python 物件結構的二進位制協議。 “Pickling” 是指將 Python 物件層次結構轉換為位元組流的過程,而 “unpickling” 是逆向操作,即透過位元組流(來自二進位制檔案位元組類物件)將其轉換回物件層次結構。Pickling(和 unpickling)也稱為 “序列化”、“封送處理”[1]或“扁平化”;然而,為了避免混淆,此處使用的術語是“pickling”和“unpickling”。

警告

pickle 模組不安全。僅反序列化您信任的資料。

可以構造惡意 pickle 資料,這些資料將在反序列化期間執行任意程式碼。切勿反序列化可能來自不可信來源或可能已被篡改的資料。

如果您需要確保資料未被篡改,請考慮使用 hmac 對資料進行簽名。

如果您正在處理不可信資料,則更安全的序列化格式(例如 json)可能更合適。請參閱與 json 的比較

與其他 Python 模組的關係

marshal 的比較

Python 有一個更原始的序列化模組,名為 marshal,但通常情況下,pickle 始終是序列化 Python 物件的首選方式。marshal 主要用於支援 Python 的 .pyc 檔案。

pickle 模組與 marshal 在幾個重要方面有所不同

  • pickle 模組會跟蹤它已經序列化的物件,這樣對同一物件的後續引用就不會再次序列化。marshal 不會這樣做。

    這對於遞迴物件和物件共享都有影響。遞迴物件是包含對自身引用的物件。Marshal 不處理這些物件,實際上,嘗試封送遞迴物件會使您的 Python 直譯器崩潰。當在要序列化的物件層次結構的不同位置有對同一物件的多個引用時,就會發生物件共享。pickle 只儲存此類物件一次,並確保所有其他引用都指向主副本。共享物件保持共享,這對於可變物件來說可能非常重要。

  • marshal 不能用於序列化使用者定義的類及其例項。pickle 可以透明地儲存和恢復類例項,但類定義必須是可匯入的,並且與物件儲存時位於同一模組中。

  • marshal 序列化格式不保證在 Python 版本之間可移植。由於其主要作用是支援 .pyc 檔案,Python 實現者保留在需要時以不向後相容的方式更改序列化格式的權利。pickle 序列化格式保證在 Python 版本之間向後相容,前提是選擇了相容的 pickle 協議,並且如果您的資料跨越了獨特的重大更改語言邊界,則 pickling 和 unpickling 程式碼會處理 Python 2 到 Python 3 的型別差異。

json 的比較

pickle 協議和 JSON (JavaScript Object Notation) 之間存在根本區別

  • JSON 是一種文字序列化格式(它輸出 Unicode 文字,儘管大多數時候它被編碼為 utf-8),而 pickle 是一種二進位制序列化格式;

  • JSON 是人類可讀的,而 pickle 不是;

  • JSON 具有互操作性,並在 Python 生態系統之外廣泛使用,而 pickle 是 Python 特有的;

  • JSON 預設只能表示 Python 內建型別的一個子集,並且不能表示自定義類;pickle 可以表示極其大量的 Python 型別(其中許多是透過巧妙利用 Python 的自省功能自動實現的;複雜的情況可以透過實現特定的物件 API 來解決);

  • 與 pickle 不同,反序列化不受信任的 JSON 本身不會造成任意程式碼執行漏洞。

參見

json 模組:一個允許 JSON 序列化和反序列化的標準庫模組。

資料流格式

pickle 使用的資料格式是 Python 特有的。這有一個優點,即不受 JSON 等外部標準的限制(JSON 無法表示指標共享);但是,這意味著非 Python 程式可能無法重構 pickled Python 物件。

預設情況下,pickle 資料格式使用相對緊湊的二進位制表示。如果您需要最佳大小特性,您可以高效地壓縮 pickled 資料。

模組 pickletools 包含用於分析 pickle 生成的資料流的工具。pickletools 原始碼包含關於 pickle 協議使用的操作碼的詳細註釋。

目前有 6 種不同的協議可用於 pickling。使用的協議版本越高,讀取生成的 pickle 所需的 Python 版本就越新。

  • 協議版本 0 是最初的“人類可讀”協議,並向後相容早期版本的 Python。

  • 協議版本 1 是一種舊的二進位制格式,也相容早期版本的 Python。

  • 協議版本 2 在 Python 2.3 中引入。它提供了更高效的 新式類 pickling。有關協議 2 帶來的改進資訊,請參閱PEP 307

  • 協議版本 3 在 Python 3.0 中新增。它明確支援 bytes 物件,並且無法由 Python 2.x 反序列化。這是 Python 3.0–3.7 中的預設協議。

  • 協議版本 4 在 Python 3.4 中新增。它增加了對超大物件的支援、更多種類物件的 pickling 以及一些資料格式最佳化。這是 Python 3.8–3.13 中的預設協議。有關協議 4 帶來的改進資訊,請參閱PEP 3154

  • 協議版本 5 在 Python 3.8 中新增。它增加了對帶外資料的支援,並加快了帶內資料的速度。它是 Python 3.14 及更高版本中的預設協議。有關協議 5 帶來的改進資訊,請參閱PEP 574

備註

序列化是一個比持久化更原始的概念;儘管 pickle 讀寫檔案物件,但它不處理命名持久化物件的問題,也不處理(更復雜)併發訪問持久化物件的問題。pickle 模組可以將複雜物件轉換為位元組流,也可以將位元組流轉換為具有相同內部結構的物件。將這些位元組流寫入檔案可能是最明顯的做法,但也可以考慮透過網路傳送它們或將其儲存在資料庫中。shelve 模組提供了一個簡單的介面,用於在 DBM 風格的資料庫檔案中對物件進行 pickle 和 unpickle。

模組介面

要序列化物件層次結構,您只需呼叫 dumps() 函式。同樣,要反序列化資料流,您只需呼叫 loads() 函式。但是,如果您希望對序列化和反序列化有更多控制,您可以分別建立 PicklerUnpickler 物件。

pickle 模組提供以下常量

pickle.HIGHEST_PROTOCOL

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

pickle.DEFAULT_PROTOCOL

一個整數,表示 pickling 時使用的預設協議版本。可能小於 HIGHEST_PROTOCOL。目前預設協議是 5,在 Python 3.8 中引入,與以前的版本不相容。此版本引入了對帶外緩衝區的支援,其中 PEP 3118 相容資料可以與主 pickle 流分離傳輸。

版本 3.0 中的變更: 預設協議為 3。

版本 3.8 中的變更: 預設協議為 4。

版本 3.14 中的變更: 預設協議為 5。

pickle 模組提供以下函式,使 pickling 過程更加方便

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

將物件 obj 的 pickled 表示寫入開啟的檔案物件 file。這等價於 Pickler(file, protocol).dump(obj)

引數 fileprotocolfix_importsbuffer_callbackPickler 建構函式中的含義相同。

版本 3.8 中的變更: 添加了 buffer_callback 引數。

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

返回物件 obj 的 pickled 表示作為 bytes 物件,而不是將其寫入檔案。

引數 protocolfix_importsbuffer_callbackPickler 建構函式中的含義相同。

版本 3.8 中的變更: 添加了 buffer_callback 引數。

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

從開啟的檔案物件 file 讀取物件的 pickled 表示,並返回其中指定的重構物件層次結構。這等價於 Unpickler(file).load()

pickle 的協議版本會自動檢測,因此無需協議引數。物件 pickled 表示之後的位元組將被忽略。

引數 filefix_importsencodingerrorsstrictbuffersUnpickler 建構函式中的含義相同。

版本 3.8 中的變更: 添加了 buffers 引數。

pickle.loads(data, /, *, fix_imports=True, encoding='ASCII', errors='strict', buffers=None)

返回物件 pickled 表示 data 的重構物件層次結構。data 必須是 位元組類物件

pickle 的協議版本會自動檢測,因此無需協議引數。物件 pickled 表示之後的位元組將被忽略。

引數 fix_importsencodingerrorsstrictbuffersUnpickler 建構函式中的含義相同。

版本 3.8 中的變更: 添加了 buffers 引數。

pickle 模組定義了三個異常

exception pickle.PickleError

其他 pickling 異常的通用基類。它繼承自 Exception

exception pickle.PicklingError

Pickler 遇到不可 pickled 的物件時引發的錯誤。它繼承自 PickleError

請參閱哪些物件可以 pickled 和 unpickled?以瞭解哪些型別的物件可以 pickled。

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,pickle 將嘗試將新的 Python 3 名稱對映到 Python 2 中使用的舊模組名稱,以便 Python 2 可以讀取 pickle 資料流。

如果 buffer_callbackNone (預設值),則緩衝區檢視將作為 pickle 流的一部分序列化到 file 中。

如果 buffer_callback 不為 None,則它可以被呼叫任意次,並帶有一個緩衝區檢視。如果回撥函式返回一個假值(例如 None),則給定緩衝區是帶外的;否則,緩衝區將帶內序列化,即在 pickle 流內部序列化。

如果 buffer_callback 不為 NoneprotocolNone 或小於 5,則會引發錯誤。

版本 3.8 中的變更: 添加了 buffer_callback 引數。

dump(obj)

obj 的 pickled 表示寫入建構函式中給定的開啟檔案物件。

persistent_id(obj)

預設情況下不執行任何操作。此方法旨在供子類覆蓋。

如果 persistent_id() 返回 None,則 obj 將照常被 pickle。任何其他值都會導致 Pickler 將返回的值作為 obj 的持久 ID 發出。此持久 ID 的含義應由 Unpickler.persistent_load() 定義。請注意,persistent_id() 返回的值本身不能具有持久 ID。

有關詳細資訊和使用示例,請參閱外部物件的持久化

版本 3.13 中的變更: Pickler 的 C 實現中添加了此方法的預設實現。

dispatch_table

一個 pickler 物件的排程表是 縮減函式 的登錄檔,這種函式可以使用 copyreg.pickle() 宣告。它是一個對映,其鍵是類,值是縮減函式。縮減函式接受關聯類的單個引數,並且應符合與 __reduce__() 方法相同的介面。

預設情況下,pickler 物件不會有 dispatch_table 屬性,而是會使用由 copyreg 模組管理的全域性排程表。但是,為了自定義特定 pickler 物件的 pickling 行為,可以將 dispatch_table 屬性設定為一個類似字典的物件。或者,如果 Pickler 的子類具有 dispatch_table 屬性,則它將用作該類例項的預設排程表。

有關使用示例,請參閱排程表

在 3.3 版本加入。

reducer_override(obj)

可在 Pickler 子類中定義的特殊縮減器。此方法優先於 dispatch_table 中的任何縮減器。它應符合與 __reduce__() 方法相同的介面,並且可以選擇返回 NotImplemented 以回退到 dispatch_table 註冊的縮減器來 pickle obj

有關詳細示例,請參閱型別、函式和其他物件的自定義縮減

在 3.8 版本加入。

fast

已棄用。如果設定為真值,則啟用快速模式。快速模式停用備忘錄的使用,從而透過不生成多餘的 PUT 操作碼來加快 pickling 過程。它不應與自引用物件一起使用,否則會導致 Pickler 無限遞迴。

如果您需要更緊湊的 pickle,請使用 pickletools.optimize()

clear_memo()

清除 pickler 的“備忘錄”。

備忘錄是記錄 pickler 已經見過的物件的資料結構,因此共享或遞迴物件是透過引用而不是值來 pickle 的。此方法在重複使用 pickler 時很有用。

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

這接受一個二進位制檔案用於讀取 pickle 資料流。

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 pickled 的 8 位字串例項;它們預設分別為 ‘ASCII’ 和 ‘strict’。encoding 可以是 ‘bytes’ 以將這些 8 位字串例項讀取為 bytes 物件。對於解 pickle NumPy 陣列和 Python 2 pickled 的 datetimedatetime 例項,需要使用 encoding='latin1'

如果 buffersNone (預設值),則反序列化所需的所有資料必須包含在 pickle 流中。這意味著當例項化 Pickler 時 (或當呼叫 dump()dumps() 時),buffer_callback 引數為 None

如果 buffers 不為 None,它應該是一個可迭代的、支援緩衝區的物件序列,每次 pickle 流引用帶外緩衝區檢視時都會被消耗。這些緩衝區已按順序提供給 Pickler 物件的 buffer_callback

版本 3.8 中的變更: 添加了 buffers 引數。

load()

從建構函式中給定的開啟檔案物件讀取物件的 pickled 表示,並返回其中指定的重構物件層次結構。物件 pickled 表示之後的位元組將被忽略。

persistent_load(pid)

預設情況下引發 UnpicklingError

如果定義了,persistent_load() 應該返回由持久 ID pid 指定的物件。如果遇到無效的持久 ID,則應引發 UnpicklingError

有關詳細資訊和使用示例,請參閱外部物件的持久化

版本 3.13 中的變更: Unpickler 的 C 實現中添加了此方法的預設實現。

find_class(module, name)

如果需要,匯入 module 並從中返回名為 name 的物件,其中 modulename 引數是 str 物件。請注意,與其名稱暗示的不同,find_class() 也用於查詢函式。

子類可以覆蓋此方法,以控制載入何種型別的物件以及如何載入它們,從而可能降低安全風險。有關詳細資訊,請參閱限制全域性變數

引發一個審計事件 pickle.find_class,引數為 module, name

class pickle.PickleBuffer(buffer)

一個表示可 pickle 資料的緩衝區的包裝器。buffer 必須是一個提供緩衝區的物件,例如位元組類物件或 N 維陣列。

PickleBuffer 物件本身就是一個緩衝區提供者,因此可以將其傳遞給其他期望提供緩衝區的 API,例如 memoryview

PickleBuffer 物件只能使用 pickle 協議 5 或更高版本進行序列化。它們適用於帶外序列化

在 3.8 版本加入。

raw()

返回此緩衝區底層記憶體區域的 memoryview。返回的物件是一個一維、C 連續的 memoryview,格式為 B (無符號位元組)。如果緩衝區既不是 C 連續也不是 Fortran 連續,則會引發 BufferError

release()

釋放 PickleBuffer 物件暴露的底層緩衝區。

哪些物件可以 pickled 和 unpickled?

以下型別可以 pickled

  • 內建常量(NoneTrueFalseEllipsisNotImplemented);

  • 整數、浮點數、複數;

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

  • 只包含可 pickle 物件的元組、列表、集合和字典;

  • 可從模組頂層訪問的函式(內建和使用者定義)(使用 def,而不是 lambda);

  • 可從模組頂層訪問的類;

  • 這些類的例項,其呼叫 __getstate__() 的結果是可 pickle 的(詳見pickling 類例項部分)。

嘗試 pickle 不可 pickle 的物件將引發 PicklingError 異常;發生這種情況時,可能已經有未指定數量的位元組寫入到底層檔案。嘗試 pickle 高度遞迴的資料結構可能會超出最大遞迴深度,在這種情況下將引發 RecursionError。您可以謹慎地使用 sys.setrecursionlimit() 提高此限制。

請注意,函式(內建和使用者定義的)是透過完全限定名稱進行 pickle 的,而不是透過值。[2] 這意味著只 pickle 函式名稱,以及包含模組和類的名稱。函式的程式碼或其任何函式屬性都不會被 pickle。因此,定義模組必須在 unpickling 環境中可匯入,並且模組必須包含命名的物件,否則將引發異常。[3]

同樣,類是透過完全限定名稱進行 pickle 的,因此 unpickling 環境中的限制也適用。請注意,類的程式碼或資料都不會被 pickle,因此在以下示例中,類屬性 attr 在 unpickling 環境中不會被恢復

class Foo:
    attr = 'A class attribute'

picklestring = pickle.dumps(Foo)

這些限制是可 pickle 的函式和類必須在模組頂層定義的原因。

同樣,當類例項被 pickle 時,它們的類程式碼和資料不會隨它們一起被 pickle。只有例項資料被 pickle。這是有意為之的,因此您可以修復類中的錯誤或向類新增方法,並且仍然可以載入使用早期版本類建立的物件。如果您計劃擁有長生命週期的物件,這些物件將經歷多個版本的類,那麼在物件中放置版本號可能是有意義的,以便類的 __setstate__() 方法可以進行適當的轉換。

Pickling 類例項

在本節中,我們將介紹可用於定義、定製和控制類例項如何被 pickle 和 unpickle 的通用機制。

在大多數情況下,無需額外的程式碼即可使例項可 pickle。預設情況下,pickle 將透過自省檢索類的屬性和例項的屬性。當類例項被 unpickle 時,其 __init__() 方法通常會被呼叫。預設行為首先建立一個未初始化的例項,然後恢復儲存的屬性。以下程式碼顯示了此行為的實現

def save(obj):
    return (obj.__class__, obj.__dict__)

def restore(cls, attributes):
    obj = cls.__new__(cls)
    obj.__dict__.update(attributes)
    return obj

類可以透過提供一個或多個特殊方法來改變預設行為

object.__getnewargs_ex__()

在協議 2 及更高版本中,實現了 __getnewargs_ex__() 方法的類可以指定在 unpickling 時傳遞給 __new__() 方法的值。該方法必須返回一對 (args, kwargs),其中 args 是位置引數的元組,kwargs 是用於構造物件的命名引數的字典。這些引數將在 unpickling 時傳遞給 __new__() 方法。

如果您的類的 __new__() 方法需要僅限關鍵字的引數,您應該實現此方法。否則,為了相容性,建議實現 __getnewargs__()

版本 3.6 中的變更: __getnewargs_ex__() 現在在協議 2 和 3 中使用。

object.__getnewargs__()

此方法的作用與 __getnewargs_ex__() 類似,但僅支援位置引數。它必須返回一個引數元組 args,該元組將在 unpickling 時傳遞給 __new__() 方法。

如果定義了 __getnewargs_ex__(),則不會呼叫 __getnewargs__()

版本 3.6 中的變更: 在 Python 3.6 之前,在協議 2 和 3 中呼叫的是 __getnewargs__() 而不是 __getnewargs_ex__()

object.__getstate__()

類可以透過重寫方法 __getstate__() 進一步影響其例項如何被 pickle。它會被呼叫,並且返回的物件將作為例項的內容被 pickle,而不是預設狀態。有幾種情況

  • 對於沒有例項 __dict__ 也沒有 __slots__ 的類,預設狀態為 None

  • 對於具有例項 __dict__ 但沒有 __slots__ 的類,預設狀態是 self.__dict__

  • 對於同時具有例項 __dict____slots__ 的類,預設狀態是一個由兩個字典組成的元組:self.__dict__ 和一個將槽名對映到槽值的字典。後者只包含有值的槽。

  • 對於具有 __slots__ 而沒有例項 __dict__ 的類,預設狀態是一個元組,其第一個項是 None,第二個項是上一點中描述的將槽名對映到槽值的字典。

版本 3.11 中的變更: object 類中添加了 __getstate__() 方法的預設實現。

object.__setstate__(state)

在 unpickling 時,如果類定義了 __setstate__(),則會使用 unpickled 狀態呼叫它。在這種情況下,狀態物件沒有必要是字典。否則,pickled 狀態必須是字典,並且其項將分配給新例項的字典。

備註

如果 __reduce__() 在 pickling 時返回狀態值為 None,則在 unpickling 時不會呼叫 __setstate__() 方法。

有關如何使用方法 __getstate__()__setstate__() 的更多資訊,請參閱處理有狀態物件部分。

備註

在 unpickling 時,例項上可能會呼叫一些方法,例如 __getattr__()__getattribute__()__setattr__()。如果這些方法依賴於某個內部不變數為真,則型別應實現 __new__() 來建立此不變數,因為在 unpickling 例項時不會呼叫 __init__()

正如我們將看到的,pickle 不會直接使用上述方法。實際上,這些方法是複製協議的一部分,該協議實現了 __reduce__() 特殊方法。複製協議為檢索 pickle 和複製物件所需的資料提供了統一的介面。[4]

儘管功能強大,但直接在類中實現 __reduce__() 容易出錯。因此,類設計者應儘可能使用高階介面(即 __getnewargs_ex__()__getstate__()__setstate__())。然而,我們將展示一些情況下,使用 __reduce__() 是唯一的選擇或導致更高效的 pickling,或兩者兼而有之。

object.__reduce__()

介面目前定義如下。 __reduce__() 方法不接受任何引數,並且應返回字串或最好是元組(返回的物件通常稱為“縮減值”)。

如果返回一個字串,則該字串應被解釋為全域性變數的名稱。它應該是物件相對於其模組的本地名稱;pickle 模組會在模組名稱空間中搜索以確定物件的模組。此行為通常對於單例很有用。

當返回一個元組時,它的長度必須在兩到六項之間。可選項可以省略,也可以提供 None 作為其值。每個項的語義按順序如下

  • 將要呼叫以建立物件初始版本的可呼叫物件。

  • 可呼叫物件的引數元組。如果可呼叫物件不接受任何引數,則必須提供一個空元組。

  • 可選地,物件的內部狀態,該狀態將如前所述傳遞給物件的 __setstate__() 方法。如果物件沒有這樣的方法,那麼該值必須是字典,並且它將被新增到物件的 __dict__ 屬性中。

  • 可選地,一個迭代器(而不是序列)生成連續的項。這些項將使用 obj.append(item) 或批次使用 obj.extend(list_of_items) 附加到物件。這主要用於列表子類,但只要它們具有具有適當簽名的 append()extend() 方法,也可以用於其他類。(使用 append() 還是 extend() 取決於使用的 pickle 協議版本以及要附加的項數,因此兩者都必須支援。)

  • 可選地,一個迭代器(不是序列)生成連續的鍵值對。這些項將使用 obj[key] = value 儲存到物件中。這主要用於字典子類,但只要它們實現了 __setitem__(),也可以用於其他類。

  • 可選地,一個帶有 (obj, state) 簽名的可呼叫物件。此可呼叫物件允許使用者以程式設計方式控制特定物件的狀態更新行為,而不是使用 obj 的靜態 __setstate__() 方法。如果不是 None,此可呼叫物件將優先於 obj__setstate__()

    版本 3.8 新增: 添加了可選的第六個元組項,(obj, state)

object.__reduce_ex__(protocol)

或者,可以定義一個 __reduce_ex__() 方法。唯一的區別是此方法應接受一個整數引數,即協議版本。如果定義了此方法,pickle 將優先於 __reduce__() 方法。此外,__reduce__() 自動成為擴充套件版本的同義詞。此方法的主要用途是為舊版 Python 版本提供向後相容的縮減值。

外部物件的持久化

為了實現物件的持久化,pickle 模組支援對序列化資料流之外的物件進行引用。這些物件透過持久 ID 進行引用,持久 ID 對於協議 0 應該是字母數字字串[5],對於任何更新的協議可以是任意物件。

此類持久 ID 的解析不由 pickle 模組定義;它會將此解析委託給序列化器和反序列化器中使用者定義的方法,即 persistent_id()persistent_load()

要序列化具有外部持久 ID 的物件,序列化器必須具有自定義的 persistent_id() 方法,該方法將物件作為引數,並返回 None 或該物件的持久 ID。當返回 None 時,序列化器會像往常一樣序列化物件。當返回持久 ID 字串時,序列化器將序列化該物件,並帶有一個標記,以便反序列化器將其識別為持久 ID。

要反序列化外部物件,反序列化器必須具有自定義的 persistent_load() 方法,該方法接受持久 ID 物件並返回引用的物件。

這是一個綜合示例,展示瞭如何使用持久 ID 透過引用序列化外部物件。

# Simple example presenting how persistent ID can be used to pickle
# external objects by reference.

import pickle
import sqlite3
from collections import namedtuple

# Simple class representing a record in our database.
MemoRecord = namedtuple("MemoRecord", "key, task")

class DBPickler(pickle.Pickler):

    def persistent_id(self, obj):
        # Instead of pickling MemoRecord as a regular class instance, we emit a
        # persistent ID.
        if isinstance(obj, MemoRecord):
            # Here, our persistent ID is simply a tuple, containing a tag and a
            # key, which refers to a specific record in the database.
            return ("MemoRecord", obj.key)
        else:
            # If obj does not have a persistent ID, return None. This means obj
            # needs to be pickled as usual.
            return None


class DBUnpickler(pickle.Unpickler):

    def __init__(self, file, connection):
        super().__init__(file)
        self.connection = connection

    def persistent_load(self, pid):
        # This method is invoked whenever a persistent ID is encountered.
        # Here, pid is the tuple returned by DBPickler.
        cursor = self.connection.cursor()
        type_tag, key_id = pid
        if type_tag == "MemoRecord":
            # Fetch the referenced record from the database and return it.
            cursor.execute("SELECT * FROM memos WHERE key=?", (str(key_id),))
            key, task = cursor.fetchone()
            return MemoRecord(key, task)
        else:
            # Always raises an error if you cannot return the correct object.
            # Otherwise, the unpickler will think None is the object referenced
            # by the persistent ID.
            raise pickle.UnpicklingError("unsupported persistent object")


def main():
    import io
    import pprint

    # Initialize and populate our database.
    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,))

    # Fetch the records to be pickled.
    cursor.execute("SELECT * FROM memos")
    memos = [MemoRecord(key, task) for key, task in cursor]
    # Save the records using our custom DBPickler.
    file = io.BytesIO()
    DBPickler(file).dump(memos)

    print("Pickled records:")
    pprint.pprint(memos)

    # Update a record, just for good measure.
    cursor.execute("UPDATE memos SET task='learn italian' WHERE key=1")

    # Load the records from the pickle data stream.
    file.seek(0)
    memos = DBUnpickler(file, conn).load()

    print("Unpickled records:")
    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:
    """Print and number lines in a text file."""

    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):
        # Copy the object's state from self.__dict__ which contains
        # all our instance attributes. Always use the dict.copy()
        # method to avoid modifying the original state.
        state = self.__dict__.copy()
        # Remove the unpicklable entries.
        del state['file']
        return state

    def __setstate__(self, state):
        # Restore instance attributes (i.e., filename and lineno).
        self.__dict__.update(state)
        # Restore the previously opened file's state. To do so, we need to
        # reopen it and read from it until the line count is restored.
        file = open(self.filename)
        for _ in range(self.lineno):
            file.readline()
        # Finally, save the file.
        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):
        """Custom reducer for MyClass."""
        if getattr(obj, "__name__", None) == "MyClass":
            return type, (obj.__name__, obj.__bases__,
                          {'my_attribute': obj.my_attribute})
        else:
            # For any other object, fallback to usual reduction
            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

帶外緩衝區

在 3.8 版本加入。

在某些情況下,pickle 模組用於傳輸大量資料。因此,最小化記憶體複製次數以保持效能和資源消耗非常重要。然而,pickle 模組的正常操作,因為它將物件的圖結構轉換為位元組的順序流,本質上涉及將資料複製到序列化流和從中複製資料。

如果 提供者 (要傳輸的物件型別的實現)和 消費者 (通訊系統的實現)都支援 pickle 協議 5 及更高版本提供的帶外傳輸功能,則可以避免此限制。

提供者 API

要序列化的大資料物件必須實現專門用於協議 5 及更高版本的 __reduce_ex__() 方法,該方法為任何大資料返回一個 PickleBuffer 例項(而不是例如 bytes 物件)。

一個 PickleBuffer 物件 表示 底層緩衝區符合帶外資料傳輸的條件。這些物件與 pickle 模組的正常使用相容。但是,消費者也可以選擇告知 pickle 他們將自行處理這些緩衝區。

消費者 API

通訊系統可以啟用對序列化物件圖時生成的 PickleBuffer 物件的自定義處理。

在傳送方,它需要將 buffer_callback 引數傳遞給 Pickler (或傳遞給 dump()dumps() 函式),該引數將在序列化物件圖時生成的每個 PickleBuffer 中呼叫。由 buffer_callback 累積的緩衝區將不會將其資料複製到 pickle 流中,只會插入一個廉價的標記。

在接收方,它需要將 buffers 引數傳遞給 Unpickler (或傳遞給 load()loads() 函式),這是一個 buffers 可迭代物件,其中包含傳遞給 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 is forbidden with pickle protocols <= 4.
            return type(self)._reconstruct, (bytearray(self),)

    @classmethod
    def _reconstruct(cls, obj):
        with memoryview(obj) as m:
            # Get a handle over the original buffer object
            obj = m.obj
            if type(obj) is cls:
                # Original buffer object is a ZeroCopyByteArray, return it
                # as-is.
                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: a copy was made

但是,如果我們傳遞一個 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: no copy was made

此示例受 bytearray 分配自身記憶體的事實限制:你無法建立由另一個物件的記憶體支援的 bytearray 例項。然而,第三方資料型別(如 NumPy 陣列)沒有此限制,並允許在不同程序或系統之間傳輸時使用零複製序列化(或儘可能少的複製)。

參見

PEP 574 – 帶外資料的 Pickle 協議 5

限制全域性變數

預設情況下,反序列化將匯入它在序列化資料中找到的任何類或函式。對於許多應用程式,此行為是不可接受的,因為它允許反序列化器匯入和呼叫任意程式碼。只需考慮這個手工製作的序列化資料流在載入時會做什麼

>>> 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 模組中載入少量安全類

import builtins
import io
import pickle

safe_builtins = {
    'range',
    'complex',
    'set',
    'frozenset',
    'slice',
}

class RestrictedUnpickler(pickle.Unpickler):

    def find_class(self, module, name):
        # Only allow safe classes from builtins.
        if module == "builtins" and name in safe_builtins:
            return getattr(builtins, name)
        # Forbid everything else.
        raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
                                     (module, name))

def restricted_loads(s):
    """Helper function analogous to 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 中的編組 API 或第三方解決方案。

效能

最近的 pickle 協議版本(從協議 2 及以上)為幾種常見功能和內建型別提供了高效的二進位制編碼。此外,pickle 模組有一個用 C 編寫的透明最佳化器。

示例

對於最簡單的程式碼,請使用 dump()load() 函式。

import pickle

# An arbitrary collection of objects supported by pickle.
data = {
    'a': [1, 2.0, 3+4j],
    'b': ("character string", b"byte string"),
    'c': {None, True, False}
}

with open('data.pickle', 'wb') as f:
    # Pickle the 'data' dictionary using the highest protocol available.
    pickle.dump(data, f, pickle.HIGHEST_PROTOCOL)

以下示例讀取生成的序列化資料。

import pickle

with open('data.pickle', 'rb') as f:
    # The protocol version used is detected automatically, so we do not
    # have to specify it.
    data = pickle.load(f)

命令列介面

pickle 模組可以作為命令列指令碼呼叫,它將顯示 pickle 檔案的內容。但是,當您要檢查的 pickle 檔案來自不受信任的源時,-m pickletools 是一個更安全的選擇,因為它不執行 pickle 位元組碼,請參閱 pickletools CLI 使用

python -m pickle pickle_file [pickle_file ...]

接受以下選項

pickle_file

要讀取的 pickle 檔案,或 - 表示從標準輸入讀取。

參見

模組 copyreg

用於擴充套件型別的 Pickle 介面建構函式註冊。

模組 pickletools

用於處理和分析序列化資料的工具。

模組 shelve

索引物件資料庫;使用 pickle

模組 copy

淺層和深層物件複製。

模組 marshal

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

腳註