pickle — Python 物件序列化

原始碼: Lib/pickle.py


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

警告

pickle 模組不安全。只 unpickle 你信任的資料。

可以構造惡意的 pickle 資料,這些資料在 unpickling 期間會執行任意程式碼。永遠不要 unpickle 可能來自不可信來源或可能被篡改的資料。

如果你需要確保資料沒有被篡改,請考慮使用 hmac 簽署資料。

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

與其他 Python 模組的關係

marshal 的比較

Python 有一個更原始的序列化模組,稱為 marshal,但通常 pickle 應該是序列化 Python 物件的首選方式。marshal 的存在主要是為了支援 Python 的 .pyc 檔案。

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

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

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

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

  • marshal 序列化格式不能保證在 Python 版本之間可移植。由於它的主要工作是支援 .pyc 檔案,因此 Python 實現者保留在必要時以不向後相容的方式更改序列化格式的權利。如果選擇了相容的 pickle 協議,並且 pickling 和 unpickling 程式碼處理 Python 2 到 Python 3 的型別差異(如果你的資料跨越了這種獨特的突破性語言邊界),則 pickle 序列化格式保證在 Python 版本之間向後相容。

json 的比較

pickle 協議和 JSON(JavaScript 物件表示法)之間存在根本差異

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

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

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

  • 預設情況下,JSON 只能表示 Python 內建型別的一個子集,而不能表示任何自定義類;pickle 可以表示數量非常龐大的 Python 型別(其中許多型別透過巧妙地使用 Python 的自省工具自動錶示;複雜的案例可以透過實現特定的物件 API來解決);

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

另請參閱

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

資料流格式

pickle 使用的資料格式是 Python 特有的。這樣做的好處是不受外部標準(如 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 中新增。它增加了對超大物件的支援、可以序列化更多型別的物件以及一些資料格式最佳化。它是從 Python 3.8 開始的預設協議。有關協議 4 帶來的改進,請參閱 PEP 3154

  • 協議版本 5 在 Python 3.8 中新增。它增加了對帶外資料的支援以及對帶內資料的加速。有關協議 5 帶來的改進,請參閱 PEP 574

注意

序列化是一個比持久化更原始的概念;雖然 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)

將物件 obj 的序列化表示寫入開啟的 檔案物件 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)

bytes 物件的形式返回物件 obj 的序列化表示,而不是將其寫入檔案。

引數 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 的協議版本,因此不需要協議引數。會忽略物件序列化表示之後的位元組。

引數 filefix_importsencodingerrorsstrictbuffers 的含義與 Unpickler 建構函式中的含義相同。

在 3.8 版本中更改: 添加了 buffers 引數。

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

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

會自動檢測 pickle 的協議版本,因此不需要協議引數。會忽略物件序列化表示之後的位元組。

引數 fix_importsencodingerrorsstrictbuffers 的含義與 Unpickler 建構函式中的含義相同。

在 3.8 版本中更改: 添加了 buffers 引數。

pickle 模組定義了三個異常

exception pickle.PickleError

其他序列化異常的通用基類。它繼承自 Exception

exception pickle.PicklingError

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

請參閱 哪些可以被序列化和反序列化? 以瞭解哪些型別的物件可以被序列化。

exception pickle.UnpicklingError

當解封(unpickling)物件時出現問題時引發的錯誤,例如資料損壞或安全違規。它繼承自 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 中使用的舊模組名稱,以便 pickle 資料流可被 Python 2 讀取。

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

如果 buffer_callback 不為 None,則可以使用緩衝區檢視多次呼叫它。如果回撥返回 false 值(例如 None),則給定的緩衝區是帶外的;否則,緩衝區將帶內序列化,即在 pickle 流內部。

如果 buffer_callback 不為 NoneprotocolNone 或小於 5,則會出錯。

在 3.8 版本中更改: 添加了 buffer_callback 引數。

dump(obj)

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

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 屬性設定為類似 dict 的物件。或者,如果 Pickler 的子類具有 dispatch_table 屬性,則它將用作該類例項的預設排程表。

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

在 3.3 版本中新增。

reducer_override(obj)

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

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

在 3.8 版本中新增。

fast

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

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

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 pickling 的 8 位字串例項;這些引數分別預設為“ASCII”和“strict”。encoding 可以為“bytes”,將這些 8 位字串例項讀取為位元組物件。對於解封 NumPy 陣列和 datetimedatetime (由 Python 2 pickling) 的例項,需要使用 encoding='latin1'

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

如果 buffers 不為 None,它應該是一個可迭代的、啟用緩衝區的物件,每次 pickle 資料流引用帶外緩衝區檢視時都會使用它。這些緩衝區已按照 Pickler 物件的 buffer_callback 的順序給出。

在 3.8 版本中更改: 添加了 buffers 引數。

load()

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

persistent_load(pid)

預設情況下引發 UnpicklingError 異常。

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

有關詳細資訊和使用示例,請參閱 外部物件的永續性

在 3.13 版本中更改: Unpickler 的 C 實現中新增此方法的預設實現。

find_class(module, name)

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

子類可以重寫此方法以控制載入的物件型別以及載入方式,從而潛在地降低安全風險。有關詳細資訊,請參閱限制全域性變數

使用引數 modulename 引發審計事件 pickle.find_class

class pickle.PickleBuffer(buffer)

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

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

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

在 3.8 版本中新增。

raw()

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

release()

釋放 PickleBuffer 物件公開的底層緩衝區。

哪些物件可以被 pickle 和 unpickle?

以下型別可以被 pickle

  • 內建常量 (None, True, False, Ellipsis, 和 NotImplemented);

  • 整數、浮點數、複數;

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

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

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

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

  • 此類類的例項,其呼叫 __getstate__() 的結果是可 pickle 的(有關詳細資訊,請參閱 Pickling 類例項 部分)。

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

請注意,函式(內建函式和使用者定義函式)是透過完全限定名稱而不是值來 pickle 的。[2] 這意味著只 pickle 函式名稱,以及包含模組和類的名稱。既不 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 版本中更改: 現在在協議 2 和 3 中使用 __getnewargs_ex__()

object.__getnewargs__()

此方法與 __getnewargs_ex__() 的作用類似,但僅支援位置引數。它必須返回一個引數元組 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__ 的類,預設狀態是由兩個字典組成的元組:self.__dict__ 和一個將槽名稱對映到槽值的字典。只有具有值的槽才會包含在後者中。

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

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

object.__setstate__(state)

在反序列化時,如果類定義了 __setstate__(),則會使用反序列化的狀態呼叫它。在這種情況下,狀態物件不需要是字典。否則,序列化的狀態必須是字典,並且其條目將分配給新例項的字典。

注意

如果 __reduce__() 在序列化時返回一個值為 None 的狀態,則在反序列化時不會呼叫 __setstate__() 方法。

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

注意

在反序列化時,可能會在例項上呼叫諸如 __getattr__()__getattribute__()__setattr__() 之類的方法。如果這些方法依賴於某些內部不變數為真,則型別應實現 __new__() 以建立這樣的不變數,因為在反序列化例項時不會呼叫 __init__()

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

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

object.__reduce__()

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

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

當返回元組時,它必須包含兩個到六個條目。可選條目可以省略,也可以提供 None 作為其值。每個條目的語義依次為:

  • 一個可呼叫物件,將呼叫該物件以建立物件的初始版本。

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

  • 可選地,物件的 state,它將傳遞給物件的 __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 版本提供向後相容的 reduce 值。

外部物件的持久化

為了方便物件的持久化,pickle 模組支援引用 pickle 資料流之外的物件的概念。此類物件透過持久 ID 引用,該 ID 應該是字母數字字元的字串(對於協議 0) [5] 或只是一個任意物件(對於任何較新的協議)。

此類持久 ID 的解析不是由 pickle 模組定義的;它會將此解析委託給 pickler 和 unpickler 上的使用者定義方法,分別為 persistent_id()persistent_load()

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

要 unpickle 外部物件,unpickler 必須具有一個自定義的 persistent_load() 方法,該方法接受一個持久 ID 物件並返回被引用的物件。

這是一個全面的示例,展示瞭如何使用持久 ID 透過引用來 pickle 外部物件。

# 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()

分發表

如果希望自定義某些類的 pickling,而不影響任何其他依賴 pickling 的程式碼,則可以建立一個具有私有分發表的 pickler。

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 模組的所有使用者共享的全域性分發表。

處理有狀態物件

這是一個示例,展示瞭如何修改類的 pickling 行為。下面的 TextReader 類開啟一個文字檔案,並在每次呼叫其 readline() 方法時返回行號和行內容。如果 pickle 一個 TextReader 例項,則儲存 除了 檔案物件成員之外的所有屬性。當 unpickle 該例項時,檔案將重新開啟,並且從上次位置恢復讀取。__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 可能不夠靈活。特別是,我們可能希望基於物件型別以外的其他標準自定義 pickling,或者我們可能希望自定義函式和類的 pickling。

對於這些情況,可以從 Pickler 類繼承並實現一個 reducer_override() 方法。此方法可以返回任意歸約元組(請參見 __reduce__())。它也可以返回 NotImplemented 以回退到傳統行為。

如果同時定義了 dispatch_tablereducer_override(),則 reducer_override() 方法優先。

注意

出於效能原因,可能不會為以下物件呼叫 reducer_override()NoneTrueFalse 以及 intfloatbytesstrdictsetfrozensetlisttuple 的確切例項。

這是一個簡單的示例,我們允許 pickle 和重建給定的類

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 流和從中複製資料。

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

提供者 API

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

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

使用者 API

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

在傳送端,需要將 buffer_callback 引數傳遞給 Pickler (或傳遞給 dump()dumps() 函式),當對物件圖進行 pickle 操作時,生成的每個 PickleBuffer 都將呼叫此引數。buffer_callback 累積的緩衝區的資料不會被複制到 pickle 流中,只會插入一個廉價的標記。

在接收端,需要將 buffers 引數傳遞給 Unpickler (或傳遞給 load()loads() 函式),該引數是傳遞給 buffer_callback 的緩衝區的可迭代物件。此可迭代物件應按照傳遞給 buffer_callback 的順序生成緩衝區。這些緩衝區將提供由物件的重構器預期的資料,這些物件的 pickle 操作生成了原始的 PickleBuffer 物件。

在傳送端和接收端之間,通訊系統可以自由地實現其自己的帶外緩衝區傳輸機制。潛在的最佳化包括使用共享記憶體或依賴於資料型別的壓縮。

示例

這是一個簡單的示例,其中我們實現了一個 bytearray 子類,該子類能夠參與帶外緩衝區 pickle 操作

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 類方法)如果緩衝區具有正確的型別,則返回緩衝區提供的物件。這是在此玩具示例中模擬零複製行為的一種簡單方法。

在使用者端,我們可以像往常一樣 pickle 這些物件,在反序列化後,我們會得到原始物件的副本

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 陣列之類的第三方資料型別沒有此限制,並且在不同的程序或系統之間傳輸時,允許使用零複製 pickle 操作(或儘可能少地複製)。

另請參閱

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

限制全域性變數

預設情況下,unpickle 操作會匯入它在 pickle 資料中找到的任何類或函式。對於許多應用程式,此行為是不可接受的,因為它允許 unpickler 匯入和呼叫任意程式碼。請考慮載入此手工製作的 pickle 資料流時會發生什麼

>>> import pickle
>>> pickle.loads(b"cos\nsystem\n(S'echo hello world'\ntR.")
hello world
0

在此示例中,unpickler 匯入 os.system() 函式,然後應用字串引數 “echo hello world”。儘管此示例是無害的,但不難想象一個可能會損害您的系統的示例。

因此,您可能需要透過自定義 Unpickler.find_class() 來控制 unpickle 的內容。與名稱所暗示的不同,每當請求全域性變數(即類或函式)時,都會呼叫 Unpickler.find_class()。因此,可以完全禁止全域性變數或將它們限制為安全的子集。

這是 unpickler 的一個示例,該 unpickler 僅允許載入來自 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()

我們 unpickler 的示例用法按預期工作

>>> 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

正如我們的示例所示,您必須小心允許 unpickle 的內容。因此,如果安全是一個問題,您可能需要考慮其他替代方案,例如 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)

以下示例讀取生成的 pickle 資料。

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)

另請參閱

模組 copyreg

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

模組 pickletools

用於處理和分析 pickle 資料的工具。

模組 shelve

物件的索引資料庫;使用 pickle

模組 copy

淺層和深層物件複製。

模組 marshal

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

腳註