pickle —— Python 對象序列化?

源代碼: Lib/pickle.py


模塊 pickle 實現了對一個 Python 對象結構的二進制序列化和反序列化。 "Pickling" 是將 Python 對象及其所擁有的層次結構轉化為一個字節流的過程,而 "unpickling" 是相反的操作,會將(來自一個 binary file 或者 bytes-like object 的)字節流轉化回一個對象層次結構。Pickling(和 unpickling)也被稱為“序列化”, “編組” 1 或者 “平面化”。而為了避免混亂,此處采用術語 “pickling” 和 “unpickling”。

警告

pickle 模塊在接受被錯誤地構造或者被惡意地構造的數據時不安全。永遠不要 unpickle 來自于不受信任的或者未經驗證的來源的數據。

與其他 Python 模塊間的關系?

marshal 間的關系?

Python 有一個更原始的序列化模塊稱為 marshal,但一般地 pickle 應該是序列化 Python 對象時的首選。marshal 存在主要是為了支持 Python 的 .pyc 文件.

pickle 模塊與 marshal 在如下幾方面顯著地不同:

  • pickle 模塊會跟蹤已被序列化的對象,所以該對象之后再次被引用時不會再次被序列化。marshal 不會這么做。

    這隱含了遞歸對象和共享對象。遞歸對象指包含對自己的引用的對象。這種對象并不會被 marshal 接受,并且實際上嘗試 marshal 遞歸對象會讓你的 Python 解釋器崩潰。對象共享發生在對象層級中存在多處引用同一對象時。pickle 只會存儲這些對象一次,并確保其他的引用指向同一個主副本。共享對象將保持共享,這可能對可變對象非常重要。

  • marshal 不能被用于序列化用戶定義類及其實例。pickle 能夠透明地存儲并保存類實例,然而此時類定義必須能夠從與被存儲時相同的模塊被引入。

  • 同樣用于序列化的 marshal 格式不保證數據能移植到不同的 Python 版本中。因為它的主要任務是支持 .pyc 文件,必要時會以破壞向后兼容的方式更改這種序列化格式,為此 Python 的實現者保留了更改格式的權利。pickle 序列化格式可以在不同版本的 Python 中實現向后兼容,前提是選擇了合適的 pickle 協議。如果你的數據要在 Python 2 與 Python 3 之間跨越傳遞,Pickling 和 Unpickling 的代碼在 2 和 3 之間也是不同的。

json 模塊的比較?

Pickle 協議和 JSON (JavaScript Object Notation) 間有著本質的不同:

  • JSON 是一個文本序列化格式(它輸出 unicode 文本,盡管在大多數時候它會接著以 utf-8 編碼),而 pickle 是一個二進制序列化格式;

  • JSON 是我們可以直觀閱讀的,而 pickle 不是;

  • JSON是可互操作的,在Python系統之外廣泛使用,而pickle則是Python專用的;

  • 默認情況下,JSON 只能表示 Python 內置類型的子集,不能表示自定義的類;但 pickle 可以表示大量的 Python 數據類型(可以合理使用 Python 的對象內省功能自動地表示大多數類型,復雜情況可以通過實現 specific object APIs 來解決)。

參見

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

數據流格式?

pickle 所使用的數據格式僅可用于 Python。這樣做的好處是沒有外部標準給該格式強加限制,比如 JSON 或 XDR(不能表示共享指針)標準;但這也意味著非 Python 程序可能無法重新讀取 pickle 打包的 Python 對象。

默認情況下,pickle 格式使用相對緊湊的二進制來存儲。如果需要讓文件更小,可以高效地 壓縮 由 pickle 打包的數據。

pickletools 模塊包含了相應的工具用于分析 pickle 生成的數據流。pickletools 源碼中包含了對 pickle 協議使用的操作碼的大量注釋。

當前用于 pickling 的協議共有 5 種。使用的協議版本越高,讀取生成的 pickle 所需的 Python 版本就要越新。

  • v0 版協議是原始的“人類可讀”協議,并且向后兼容早期版本的 Python。

  • v1 版協議是較早的二進制格式,它也與早期版本的 Python 兼容。

  • v2 版協議是在 Python 2.3 中引入的。它為存儲 new-style class 提供了更高效的機制。欲了解有關第 2 版協議帶來的改進,請參閱 PEP 307

  • v3 版協議添加于 Python 3.0。它具有對 bytes 對象的顯式支持,且無法被 Python 2.x 打開。這是目前默認使用的協議,也是在要求與其他 Python 3 版本兼容時的推薦協議。

  • v4 版協議添加于 Python 3.4。它支持存儲非常大的對象,能存儲更多種類的對象,還包括一些針對數據格式的優化。有關第 4 版協議帶來改進的信息,請參閱 PEP 3154

注解

序列化是一種比持久化更底層的概念,雖然 pickle 讀取和寫入的是文件對象,但它不處理持久對象的命名問題,也不處理對持久對象的并發訪問(甚至更復雜)的問題。pickle 模塊可以將復雜對象轉換為字節流,也可以將字節流轉換為具有相同內部結構的對象。處理這些字節流最常見的做法是將它們寫入文件,但它們也可以通過網絡發送或存儲在數據庫中。shelve 模塊提供了一個簡單的接口,用于在 DBM 類型的數據庫文件上 pickle 和 unpickle 對象。

模塊接口?

要序列化某個包含層次結構的對象,只需調用 dumps() 函數即可。同樣,要反序列化數據流,可以調用 loads() 函數。但是,如果要對序列化和反序列化加以更多的控制,可以分別創建 PicklerUnpickler 對象。

pickle 模塊包含了以下常量:

pickle.HIGHEST_PROTOCOL?

整數,可用的最高 協議版本。此值可以作為 協議 值傳遞給 dump()dumps() 函數,以及 Pickler 的構造函數。

pickle.DEFAULT_PROTOCOL?

一個整數,表示封存操作使用的 協議版本。 它可能小于 HIGHEST_PROTOCOL。 當前默認協議版本為 3,它是一個為 Python 3 設計的新協議。

pickle 模塊提供了以下方法,讓打包過程更加方便。

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

將打包好的對象 obj 寫入已打開的 file object file。它等同于 Pickler(file, protocol).dump(obj)

可選參數 protocol 是一個整數,告知 pickler 使用指定的協議,可選擇的協議范圍從 0 到 HIGHEST_PROTOCOL。如果沒有指定,這一參數默認值為 DEFAULT_PROTOCOL。指定一個負數就相當于指定 HIGHEST_PROTOCOL

參數 file 必須有一個 write() 方法,該 write() 方法要能接收字節作為其唯一參數。因此,它可以是一個打開的磁盤文件(用于寫入二進制內容),也可以是一個 io.BytesIO 實例,也可以是滿足這一接口的其他任何自定義對象。

如果 fix_imports 為 True 且 protocol 小于 3,pickle 將嘗試將 Python 3 中的新名稱映射到 Python 2 中的舊模塊名稱,因此 Python 2 也可以讀取打包出的數據流。

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

obj 打包以后的對象作為 bytes 類型直接返回,而不是將其寫入到文件。

參數 protocolfix_imports 的含義與它們在 dump() 中的含義相同。

pickle.load(file, *, fix_imports=True, encoding="ASCII", errors="strict")?

從已打開的 file object 文件 中讀取打包后的對象,重建其中特定對象的層次結構并返回。它相當于 Unpickler(file).load()

Pickle 協議版本是自動檢測出來的,所以不需要參數來指定協議。打包對象以外的其他字節將被忽略。

參數 file 必須有兩個方法,其中 read() 方法接受一個整數參數,而 readline() 方法不需要參數。 兩個方法都應返回字節串。 因此 file 可以是一個打開用于二進制讀取的磁盤文件、一個 io.BytesIO 對象,或者任何滿足此接口要求的其他自定義對象。

可選的關鍵字參數是 fix_imports, encodingerrors,用于控制由Python 2 生成的 pickle 流的兼容性。如果 fix_imports 為 true,則 pickle 將嘗試將舊的 Python 2 名稱映射到 Python 3 中對應的新名稱。encodingerrors 參數告訴 pickle 如何解碼 Python 2 存儲的 8 位字符串實例;這兩個參數默認分別為 'ASCII' 和 'strict'。 encoding 參數可置為 'bytes' 來將這些 8 位字符串實例讀取為字節對象。讀取 NumPy array 和 Python 2 存儲的 datetimedatetime 實例時,請使用 encoding='latin1'

pickle.loads(data, *, fix_imports=True, encoding="ASCII", errors="strict")?

重建并返回一個對象的封存表示形式 data 的對象層級結構。 data 必須為 bytes-like object

Pickle 協議版本是自動檢測出來的,所以不需要參數來指定協議。打包對象以外的其他字節將被忽略。

可選的關鍵字參數是 fix_imports, encodingerrors,用于控制由Python 2 生成的 pickle 流的兼容性。如果 fix_imports 為 true,則 pickle 將嘗試將舊的 Python 2 名稱映射到 Python 3 中對應的新名稱。encodingerrors 參數告訴 pickle 如何解碼 Python 2 存儲的 8 位字符串實例;這兩個參數默認分別為 'ASCII' 和 'strict'。 encoding 參數可置為 'bytes' 來將這些 8 位字符串實例讀取為字節對象。讀取 NumPy array 和 Python 2 存儲的 datetimedatetime 實例時,請使用 encoding='latin1'

pickle 模塊定義了以下 3 個異常:

exception pickle.PickleError?

其他 pickle 異常的基類。它是 Exception 的一個子類。

exception pickle.PicklingError?

Pickler 遇到無法解包的對象時拋出此錯誤。它是 PickleError 的子類。

參考 可以被打包/解包的對象 來了解哪些對象可以被打包。

exception pickle.UnpicklingError?

當解包出錯時拋出此異常,例如數據損壞或對象不安全。它是 PickleError 的子類。

注意,解包時可能還會拋出其他異常,包括(但不限于) AttributeError、EOFError、ImportError 和 IndexError。

pickle 模塊可導出兩個類,PicklerUnpickler:

class pickle.Pickler(file, protocol=None, *, fix_imports=True)?

它接受一個二進制文件用于寫入 pickle 數據流。

可選參數 protocol 是一個整數,告知 pickler 使用指定的協議,可選擇的協議范圍從 0 到 HIGHEST_PROTOCOL。如果沒有指定,這一參數默認值為 DEFAULT_PROTOCOL。指定一個負數就相當于指定 HIGHEST_PROTOCOL

參數 file 必須有一個 write() 方法,該 write() 方法要能接收字節作為其唯一參數。因此,它可以是一個打開的磁盤文件(用于寫入二進制內容),也可以是一個 io.BytesIO 實例,也可以是滿足這一接口的其他任何自定義對象。

如果 fix_imports 為 True 且 protocol 小于 3,pickle 將嘗試將 Python 3 中的新名稱映射到 Python 2 中的舊模塊名稱,因此 Python 2 也可以讀取打包出的數據流。

dump(obj)?

obj 打包后的內容寫入已打開的文件對象,該文件對象已經在構造函數中指定。

persistent_id(obj)?

默認什么也不做。它存在是為了讓子類可以重載它。

如果 persistent_id() 返回 Noneobj 會被照常 pickle。如果返回其他值,Pickler 會將這個函數的返回值作為 obj 的持久化 ID(Pickler 本應得到序列化數據流并將其寫入文件,若此函數有返回值,則得到此函數的返回值并寫入文件)。這個持久化 ID 的解釋應當定義在 Unpickler.persistent_load() 中(該方法定義還原對象的過程,并返回得到的對象)。注意,persistent_id() 的返回值本身不能擁有持久化 ID。

參閱 持久化外部對象 獲取詳情和使用示例。

dispatch_table?

Pickler 對象的 dispatch 表是 copyreg.pickle() 中用到的 reduction 函數 的注冊。dispatch 表本身是一個 class 到其 reduction 函數的映射鍵值對。一個 reduction 函數只接受一個參數,就是其關聯的 class,函數行為應當遵守 __reduce__() 接口規范。

Pickler 對象默認并沒有 dispatch_table 屬性,該對象默認使用 copyreg 模塊中定義的全局 dispatch 表。如果要為特定 Pickler 對象自定義序列化過程,可以將 dispatch_table 屬性設置為類字典對象(dict-like object)。另外,如果 Pickler 的子類設置了 dispatch_table 屬性,則該子類的實例會使用這個表作為默認的 dispatch 表。

參閱 Dispatch 表 獲取使用示例。

3.3 新版功能.

fast?

已棄用。設為 True 則啟用快速模式。快速模式禁用了“備忘錄” (memo) 的使用,即不生成多余的 PUT 操作碼來加快打包過程。不應將其與自指 (self-referential) 對象一起使用,否則將導致 Pickler 無限遞歸。

如果需要進一步提高 pickle 的壓縮率,請使用 pickletools.optimize()

class pickle.Unpickler(file, *, fix_imports=True, encoding="ASCII", errors="strict")?

它接受一個二進制文件用于讀取 pickle 數據流。

Pickle 協議版本是自動檢測出來的,所以不需要參數來指定協議。

參數 file 必須有兩個方法,其中 read() 方法接受一個整數參數,而 readline() 方法不需要參數。 兩個方法都應返回字節串。 因此 file 可以是一個打開用于二進制讀取的磁盤文件對象、一個 io.BytesIO 對象,或者任何滿足此接口要求的其他自定義對象。

可選的關鍵字參數有 fix_imports, encodingerrors,它們用于控制由 Python 2 所生成 pickle 流的兼容性支持。 如果 fix_imports 為真值,則 pickle 將嘗試把舊的 Python 2 名稱映射到 Python 3 所使用的新名稱。 encodingerrors 將告知 pickle 如何解碼由 Python 2 所封存的 8 位字符串實例;這兩個參數的默認值分別為 'ASCII' 和 'strict'。 encoding 可設為 'bytes' 以將這些 8 位字符串實例作為字節對象來讀取。

load()?

從構造函數中指定的文件對象里讀取打包好的對象,重建其中特定對象的層次結構并返回。打包對象以外的其他字節將被忽略。

persistent_load(pid)?

默認拋出 UnpicklingError 異常。

如果定義了此方法,persistent_load() 應當返回持久化 ID pid 所指定的對象。 如果遇到無效的持久化 ID,則應當引發 UnpicklingError

參閱 持久化外部對象 獲取詳情和使用示例。

find_class(module, name)?

如有必要,導入 module 模塊并返回其中名叫 name 的對象,其中 modulename 參數都是 str 對象。注意,不要被這個函數的名字迷惑,find_class() 同樣可以用來導入函數。

子類可以重載此方法,來控制加載對象的類型和加載對象的方式,從而盡可能降低安全風險。參閱 限制全局變量 獲取更詳細的信息。

可以被打包/解包的對象?

下列類型可以被打包:

  • NoneTrueFalse

  • 整數、浮點數、復數

  • str、byte、bytearray

  • 只包含可打包對象的集合,包括 tuple、list、set 和 dict

  • 定義在模塊頂層的函數(使用 def 定義,lambda 函數則不可以)

  • 定義在模塊頂層的內置函數

  • 定義在模塊頂層的類

  • 某些類實例,這些類的 __dict__ 屬性值或 __getstate__() 函數的返回值可以被打包(詳情參閱 打包類實例 這一段)。

嘗試打包不能被打包的對象會拋出 PicklingError 異常,異常發生時,可能有部分字節已經被寫入指定文件中。嘗試打包遞歸層級很深的對象時,可能會超出最大遞歸層級限制,此時會拋出 RecursionError 異常,可以通過 sys.setrecursionlimit() 調整遞歸層級,不過請謹慎使用這個函數,因為可能會導致解釋器崩潰。

注意,函數(內建函數或用戶自定義函數)在被打包時,引用的是函數全名。2 這意味著只有函數所在的模塊名,與函數名會被打包,函數體及其屬性不會被打包。因此,在解包的環境中,函數所屬的模塊必須是可以被導入的,而且模塊必須包含這個函數被打包時的名稱,否則會拋出異常。3

同樣的,類也只打包名稱,所以在解包環境中也有和函數相同的限制。注意,類體及其數據不會被打包,所以在下面的例子中類屬性 attr 不會存在于解包后的環境中:

class Foo:
    attr = 'A class attribute'

picklestring = pickle.dumps(Foo)

這些限制決定了為什么必須在一個模塊的頂層定義可打包的函數和類。

類似的,在打包類的實例時,其類體和類數據不會跟著實例一起被打包,只有實例數據會被打包。這樣設計是有目的的,在將來修復類中的錯誤、給類增加方法之后,仍然可以載入原來版本類實例的打包數據來還原該實例。如果你準備長期使用一個對象,可能會同時存在較多版本的類體,可以為對象添加版本號,這樣就可以通過類的 __setstate__() 方法將老版本轉換成新版本。

打包類實例?

在本節中,我們描述了可用于定義、自定義和控制如何打包和解包類實例的通用流程。

通常,使一個實例可被打包不需要附加任何代碼。Pickle 默認會通過 Python 的內省機制獲得實例的類及屬性。而當實例解包時,它的 __init__() 方法通常 不會 被調用。其默認動作是:先創建一個未初始化的實例,然后還原其屬性,下面的代碼展示了這種行為的實現機制:

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

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

類可以改變默認行為,只需定義以下一種或幾種特殊方法:

object.__getnewargs_ex__()?

對于使用第 2 版或更高版協議的 pickle,實現了 __getnewargs_ex__() 方法的類可以控制在解包時傳給 __new__() 方法的參數。本方法必須返回一對 (args, kwargs) 用于構建對象,其中 args 是表示位置參數的 tuple,而 kwargs 是表示命名參數的 dict。它們會在解包時傳遞給 __new__() 方法。

如果類的 __new__() 方法只接受關鍵字參數,則應當實現這個方法。否則,為了兼容性,更推薦實現 __getnewargs__() 方法。

在 3.6 版更改: __getnewargs_ex__() 現在可用于第 2 和第 3 版協議。

object.__getnewargs__()?

這個方法與上一個 __getnewargs_ex__() 方法類似,但僅支持位置參數。它要求返回一個 tuple 類型的 args,用于解包時傳遞給 __new__() 方法。

如果定義了 __getnewargs_ex__(),那么 __getnewargs__() 就不會被調用。

在 3.6 版更改: 在 Python 3.6 前,第 2、3 版協議會調用 __getnewargs__(),更高版本協議會調用 __getnewargs_ex__()

object.__getstate__()?

類還可以進一步控制其實例的打包過程。如果類定義了 __getstate__(),它就會被調用,其返回的對象是被當做實例內容來打包的,否則打包的是實例的 __dict__。如果 __getstate__() 未定義,實例的 __dict__ 會被照常打包。

object.__setstate__(state)?

當解包時,如果類定義了 __setstate__(),就會在已解包狀態下調用它。此時不要求實例的 state 對象必須是 dict。沒有定義此方法的話,先前打包的 state 對象必須是 dict,且該 dict 內容會在解包時賦給新實例的 __dict__。

注解

如果 __getstate__() 返回 False,那么在解包時就不會調用 __setstate__() 方法。

參考 處理有狀態的對象 一段獲取如何使用 __getstate__()__setstate__() 方法的更多信息。

注解

在解封時,實例的某些方法例如 __getattr__(), __getattribute__()__setattr__() 可能會被調用。 由于這些方法可能要求某些內部不變量為真值,因此該類型應當實現 __new__() 以建立這樣的不變量,因為當解封一個實例時 __init__() 并不會被調用。

可以看出,其實 pickle 并不直接調用上面的幾個函數。事實上,這幾個函數是復制協議的一部分,它們實現了 __reduce__() 這一特殊接口。復制協議提供了統一的接口,用于在打包或復制對象的過程中取得所需數據。4

盡管這個協議功能很強,但是直接在類中實現 __reduce__() 接口容易產生錯誤。因此,設計類時應當盡可能的使用高級接口(比如 __getnewargs_ex__()__getstate__()__setstate__())。后面仍然可以看到直接實現 __reduce__() 接口的狀況,可能別無他法,可能為了獲得更好的性能,或者兩者皆有之。

object.__reduce__()?

該接口當前定義如下。__reduce__() 方法不帶任何參數,并且應返回字符串或最好返回一個元組(返回的對象通常稱為“reduce 值”)。

如果返回字符串,該字符串會被當做一個全局變量的名稱。它應該是對象相對于其模塊的本地名稱,pickle 模塊會搜索模塊命名空間來確定對象所屬的模塊。這種行為常在單例模式使用。

當返回的是一個元組時,它的長度必須在二至五項之間。 可選項可以被省略或將值設為 None。 每項的語義分別如下所示:

  • 一個可調用對象,該對象會在創建對象的最初版本時調用。

  • 可調用對象的參數,是一個元組。如果可調用對象不接受參數,必須提供一個空元組。

  • 可選元素,用于表示對象的狀態,將被傳給前述的 __setstate__() 方法。 如果對象沒有此方法,則這個元素必須是字典類型,并會被添加至 __dict__ 屬性中。

  • 可選元素,一個返回連續項的迭代器(而不是序列)。這些項會被 obj.append(item) 逐個加入對象,或被 obj.extend(list_of_items) 批量加入對象。這個元素主要用于 list 的子類,也可以用于那些正確實現了 append()extend() 方法的類。(具體是使用 append() 還是 extend() 取決于 pickle 協議版本以及待插入元素的項數,所以這兩個方法必須同時被類支持。)

  • 可選元素,一個返回連續鍵值對的迭代器(而不是序列)。這些鍵值對將會以 obj[key] = value 的方式存儲于對象中。該元素主要用于 dict 子類,也可以用于那些實現了 __setitem__() 的類。

object.__reduce_ex__(protocol)?

作為替代選項,也可以實現 __reduce_ex__() 方法。 此方法的唯一不同之處在于它應接受一個整型參數用于指定協議版本。 如果定義了這個函數,則會覆蓋 __reduce__() 的行為。 此外,__reduce__() 方法會自動成為擴展版方法的同義詞。 這個函數主要用于為以前的 Python 版本提供向后兼容的 reduce 值。

持久化外部對象?

為了獲取對象持久化的利益, pickle 模塊支持引用已封存數據流之外的對象。 這樣的對象是通過一個持久化 ID 來引用的,它應當是一個由字母數字類字符組成的字符串 (對于第 0 版協議) 5 或是一個任意對象 (用于任意新版協議)。

pickle 模塊不提供對持久化 ID 的解析工作,它將解析工作分配給用戶定義的方法,分別是 pickler 中的 persistent_id() 方法和 unpickler 中的 persistent_load() 方法。

要通過持久化 ID 將外部對象打包,必須在 pickler 中實現 persistent_id() 方法,該方法接受需要被打包的對象作為參數,返回一個 None 或返回該對象的持久化 ID。如果返回 None,該對象會被按照默認方式打包為數據流。如果返回字符串形式的持久化 ID,則會打包這個字符串和一個標記,這樣 unpickler 才能將其識別為持久化 ID。

要解封外部對象,Unpickler 必須實現 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()

Dispatch 表?

如果想對某些類進行自定義封存,而又不想在類中增加用于封存的代碼,就可以創建帶有特殊 dispatch 表的 pickler。

copyreg 模塊的 copyreg.dispatch_table 中定義了全局 dispatch 表。因此,可以使用 copyreg.dispatch_table 修改后的副本作為自有 dispatch 表。

例如:

f = io.BytesIO()
p = pickle.Pickler(f)
p.dispatch_table = copyreg.dispatch_table.copy()
p.dispatch_table[SomeClass] = reduce_SomeClass

創建了一個帶有自有 dispatch 表的 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 的實例都會共用同一份 dispatch 表。使用 copyreg 模塊實現的等效代碼是:

copyreg.pickle(SomeClass, reduce_SomeClass)
f = io.BytesIO()
p = pickle.Pickler(f)

處理有狀態的對象?

下面的示例展示了如何修改類在封存時的行為。其中 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!'

限制全局變量?

默認情況下,解封將會導入在 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 模塊的類被加載:

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, 4+6j],
    '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)

參見

模塊 copyreg

為擴展類型提供 pickle 接口所需的構造函數。

模塊 pickletools

用于處理和分析已打包數據的工具。

模塊 shelve

帶索引的數據庫,用于存放對象,使用了 pickle 模塊。

模塊 copy

淺層 (shallow) 和深層 (deep) 復制對象操作

模塊 marshal

高效地序列化內置類型的數據。

備注

1

不要把它與 marshal 模塊混淆。

2

這就是為什么 lambda 函數不可以被打包:所有的匿名函數都有同一個名字:<lambda>

3

拋出的異常有可能是 ImportErrorAttributeError,也可能是其他異常。

4

copy 模塊使用這一協議實現淺層 (shallow) 和深層 (deep) 復制操作。

5

對字母數字類字符的限制是由于持久化 ID 在協議版本 0 中是由分行符來分隔的。 因此如果持久化 ID 中出現任何形式的分行符,封存結果就將變得無法讀取。