pickle --- Python 對象序列化?

源代碼: Lib/pickle.py


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

警告

pickle 模塊**并不安全**。你只應(yīng)該對你信任的數(shù)據(jù)進(jìn)行unpickle操作。

構(gòu)建惡意的 pickle 數(shù)據(jù)來**在解封時(shí)執(zhí)行任意代碼**是可能的。絕對不要對不信任來源的數(shù)據(jù)和可能被篡改過的數(shù)據(jù)進(jìn)行解封。

請考慮使用 hmac 來對數(shù)據(jù)進(jìn)行簽名,確保數(shù)據(jù)沒有被篡改。

在你處理不信任數(shù)據(jù)時(shí),更安全的序列化格式如 json 可能更為適合。參見 與 json 模塊的比較 。

與其他 Python 模塊間的關(guān)系?

marshal 間的關(guān)系?

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

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

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

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

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

  • 同樣用于序列化的 marshal 格式不保證數(shù)據(jù)能移植到不同的 Python 版本中。因?yàn)樗闹饕蝿?wù)是支持 .pyc 文件,必要時(shí)會以破壞向后兼容的方式更改這種序列化格式,為此 Python 的實(shí)現(xiàn)者保留了更改格式的權(quán)利。pickle 序列化格式可以在不同版本的 Python 中實(shí)現(xiàn)向后兼容,前提是選擇了合適的 pickle 協(xié)議。如果你的數(shù)據(jù)要在 Python 2 與 Python 3 之間跨越傳遞,封存和解封的代碼在 2 和 3 之間也是不同的。

json 模塊的比較?

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

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

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

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

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

  • 不像pickle,對一個(gè)不信任的JSON進(jìn)行反序列化的操作本身不會造成任意代碼執(zhí)行漏洞。

參見

json 模塊:一個(gè)允許JSON序列化和反序列化的標(biāo)準(zhǔn)庫模塊

數(shù)據(jù)流格式?

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

默認(rèn)情況下,pickle 格式使用相對緊湊的二進(jìn)制來存儲。如果需要讓文件更小,可以高效地 壓縮 由 pickle 封存的數(shù)據(jù)。

pickletools 模塊包含了相應(yīng)的工具用于分析 pickle 生成的數(shù)據(jù)流。pickletools 源碼中包含了對 pickle 協(xié)議使用的操作碼的大量注釋。

當(dāng)前共有 6 種不同的協(xié)議可用于封存操作。 使用的協(xié)議版本越高,讀取所生成 pickle 對象所需的 Python 版本就要越新。

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

  • v1 版協(xié)議是較早的二進(jìn)制格式,它也與早期版本的 Python 兼容。

  • Protocol version 2 was introduced in Python 2.3. It provides much more efficient pickling of new-style classes. Refer to PEP 307 for information about improvements brought by protocol 2.

  • v3 版協(xié)議是在 Python 3.0 中引入的。 它顯式地支持 bytes 字節(jié)對象,不能使用 Python 2.x 解封。這是 Python 3.0-3.7 的默認(rèn)協(xié)議。

  • v4 版協(xié)議添加于 Python 3.4。它支持存儲非常大的對象,能存儲更多種類的對象,還包括一些針對數(shù)據(jù)格式的優(yōu)化。它是Python 3.8使用的默認(rèn)協(xié)議。有關(guān)第 4 版協(xié)議帶來改進(jìn)的信息,請參閱 PEP 3154

  • 第 5 版協(xié)議是在 Python 3.8 中加入的。 它增加了對帶外數(shù)據(jù)的支持,并可加速帶內(nèi)數(shù)據(jù)處理。 請參閱 PEP 574 了解第 5 版協(xié)議所帶來的改進(jìn)的詳情。

備注

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

模塊接口?

要序列化某個(gè)包含層次結(jié)構(gòu)的對象,只需調(diào)用 dumps() 函數(shù)即可。同樣,要反序列化數(shù)據(jù)流,可以調(diào)用 loads() 函數(shù)。但是,如果要對序列化和反序列化加以更多的控制,可以分別創(chuàng)建 PicklerUnpickler 對象。

pickle 模塊包含了以下常量:

pickle.HIGHEST_PROTOCOL?

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

pickle.DEFAULT_PROTOCOL?

整數(shù),用于 pickle 數(shù)據(jù)的默認(rèn) 協(xié)議版本。它可能小于 HIGHEST_PROTOCOL。當(dāng)前默認(rèn)協(xié)議是 v4,它在 Python 3.4 中首次引入,與之前的版本不兼容。

在 3.0 版更改: 默認(rèn)協(xié)議版本是 3。

在 3.8 版更改: 默認(rèn)協(xié)議版本是 4。

pickle 模塊提供了以下方法,讓封存過程更加方便:

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

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

參數(shù) file、protocol、fix_importsbuffer_callback 的含義與它們在 Pickler 的構(gòu)造函數(shù)中的含義相同。

在 3.8 版更改: 加入了 buffer_callback 參數(shù)。

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

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

參數(shù) protocol、fix_importsbuffer_callback 的含義與它們在 Pickler 的構(gòu)造函數(shù)中的含義相同。

在 3.8 版更改: 加入了 buffer_callback 參數(shù)。

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

從已打開的 file object 文件 中讀取封存后的對象,重建其中特定對象的層次結(jié)構(gòu)并返回。它相當(dāng)于 Unpickler(file).load()。

Pickle 協(xié)議版本是自動檢測出來的,所以不需要參數(shù)來指定協(xié)議。封存對象以外的其他字節(jié)將被忽略。

參數(shù) file、fix_importsencoding、errors、strictbuffers 的含義與它們在 Unpickler 的構(gòu)造函數(shù)中的含義相同。

在 3.8 版更改: 加入了 buffers 參數(shù)。

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

重建并返回一個(gè)對象的封存表示形式 data 的對象層級結(jié)構(gòu)。 data 必須為 bytes-like object。

Pickle 協(xié)議版本是自動檢測出來的,所以不需要參數(shù)來指定協(xié)議。封存對象以外的其他字節(jié)將被忽略。

Arguments fix_imports, encoding, errors, strict and buffers have the same meaning as in the Unpickler constructor.

在 3.8 版更改: 加入了 buffers 參數(shù)。

pickle 模塊定義了以下 3 個(gè)異常:

exception pickle.PickleError?

其他 pickle 異常的基類。它是 Exception 的一個(gè)子類。

exception pickle.PicklingError?

當(dāng) Pickler 遇到無法解封的對象時(shí)拋出此錯(cuò)誤。它是 PickleError 的子類。

參考 可以被封存/解封的對象 來了解哪些對象可以被封存。

exception pickle.UnpicklingError?

當(dāng)解封出錯(cuò)時(shí)拋出此異常,例如數(shù)據(jù)損壞或?qū)ο蟛话踩?。它?PickleError 的子類。

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

pickle 模塊包含了 3 個(gè)類,Pickler、UnpicklerPickleBuffer

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

它接受一個(gè)二進(jìn)制文件用于寫入 pickle 數(shù)據(jù)流。

可選參數(shù) protocol 是一個(gè)整數(shù),告知 pickler 使用指定的協(xié)議,可選擇的協(xié)議范圍從 0 到 HIGHEST_PROTOCOL。如果沒有指定,這一參數(shù)默認(rèn)值為 DEFAULT_PROTOCOL。指定一個(gè)負(fù)數(shù)就相當(dāng)于指定 HIGHEST_PROTOCOL。

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

如果 fix_imports 為 True 且 protocol 小于 3,pickle 將嘗試將 Python 3 中的新名稱映射到 Python 2 中的舊模塊名稱,因此 Python 2 也可以讀取封存的數(shù)據(jù)流。

如果 buffer_callback 為 None(默認(rèn)情況),緩沖區(qū)視圖(buffer view)將會作為 pickle 流的一部分被序列化到 file 中。

如果 buffer_callback 不為 None,那它可以用緩沖區(qū)視圖調(diào)用任意次。如果某次調(diào)用返回了 False 值(例如 None),則給定的緩沖區(qū)是 帶外的,否則緩沖區(qū)是帶內(nèi)的(例如保存在了 pickle 流里面)。

如果 buffer_callback 不是 None 且 protocol 是 None 或小于 5,就會出錯(cuò)。

在 3.8 版更改: 加入了 buffer_callback 參數(shù)。

dump(obj)?

obj 封存后的內(nèi)容寫入已打開的文件對象,該文件對象已經(jīng)在構(gòu)造函數(shù)中指定。

persistent_id(obj)?

默認(rèn)無動作,子類繼承重載時(shí)使用。

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

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

dispatch_table?

Pickler 對象的 dispatch 表是 copyreg.pickle() 中用到的 reduction 函數(shù) 的注冊。dispatch 表本身是一個(gè) class 到其 reduction 函數(shù)的映射鍵值對。一個(gè) reduction 函數(shù)只接受一個(gè)參數(shù),就是其關(guān)聯(lián)的 class,函數(shù)行為應(yīng)當(dāng)遵守 __reduce__() 接口規(guī)范。

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

參閱 Dispatch 表 獲取使用示例。

3.3 新版功能.

reducer_override(obj)?

可以在 Pickler 的子類中定義的特殊 reducer。此方法的優(yōu)先級高于 dispatch_table 中的任何 reducer。它應(yīng)該與 __reduce__() 方法遵循相同的接口,它也可以返回 NotImplemented,這將使用 dispatch_table 里注冊的 reducer 來封存 obj

參閱 類型,函數(shù)和其他對象的自定義歸約 獲取詳細(xì)的示例。

3.8 新版功能.

fast?

已棄用。設(shè)為 True 則啟用快速模式??焖倌J浇昧恕皞渫洝?(memo) 的使用,即不生成多余的 PUT 操作碼來加快封存過程。不應(yīng)將其與自指 (self-referential) 對象一起使用,否則將導(dǎo)致 Pickler 無限遞歸。

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

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

它接受一個(gè)二進(jìn)制文件用于讀取 pickle 數(shù)據(jù)流。

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

參數(shù) file 必須有三個(gè)方法,read() 方法接受一個(gè)整數(shù)參數(shù),readinto() 方法接受一個(gè)緩沖區(qū)作為參數(shù),readline() 方法不需要參數(shù),這與 io.BufferedIOBase 里定義的接口是相同的。因此 file 可以是一個(gè)磁盤上用于二進(jìn)制讀取的文件,也可以是一個(gè) io.BytesIO 實(shí)例,也可以是滿足這一接口的其他任何自定義對象。

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

如果 buffers 為 None(默認(rèn)值),則反序列化所需的所有數(shù)據(jù)都必須包含在 pickle 流中。這意味著在實(shí)例化 Pickler 時(shí)(或調(diào)用 dump()dumps() 時(shí)),參數(shù) buffer_callback 為 None。

如果 buffers 不為 None,則每次 pickle 流引用 帶外 緩沖區(qū)視圖時(shí),消耗的對象都應(yīng)該是可迭代的啟用緩沖區(qū)的對象。這樣的緩沖區(qū)應(yīng)該按順序地提供給 Pickler 對象的 buffer_callback 方法。

在 3.8 版更改: 加入了 buffers 參數(shù)。

load()?

從構(gòu)造函數(shù)中指定的文件對象里讀取封存好的對象,重建其中特定對象的層次結(jié)構(gòu)并返回。封存對象以外的其他字節(jié)將被忽略。

persistent_load(pid)?

默認(rèn)拋出 UnpicklingError 異常。

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

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

find_class(module, name)?

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

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

引發(fā)一個(gè) 審計(jì)事件 pickle.find_class 附帶參數(shù) module、name。

class pickle.PickleBuffer(buffer)?

緩沖區(qū)的包裝器 (wrapper),緩沖區(qū)中包含著可封存的數(shù)據(jù)。buffer 必須是一個(gè) buffer-providing 對象,比如 bytes-like object 或多維數(shù)組。

PickleBuffer 本身就可以生成緩沖區(qū)對象,因此可以將其傳遞給需要緩沖區(qū)生成器的其他 API,比如 memoryview

PickleBuffer 對象只能用 pickle 版本 5 及以上協(xié)議進(jìn)行序列化。它們符合 帶外序列化 的條件。

3.8 新版功能.

raw()?

返回該緩沖區(qū)底層內(nèi)存區(qū)域的 memoryview。 返回的對象是一維的、C 連續(xù)布局的 memoryview,格式為 B (無符號字節(jié))。 如果緩沖區(qū)既不是 C 連續(xù)布局也不是 Fortran 連續(xù)布局的,則拋出 BufferError 異常。

release()?

釋放由 PickleBuffer 占用的底層緩沖區(qū)。

可以被封存/解封的對象?

下列類型可以被封存:

  • None, True, and False;

  • integers, floating-point numbers, complex numbers;

  • strings, bytes, bytearrays;

  • tuples, lists, sets, and dictionaries containing only picklable objects;

  • functions (built-in and user-defined) accessible from the top level of a module (using def, not lambda);

  • classes accessible from the top level of a module;

  • instances of such classes whose the result of calling __getstate__() is picklable (see section 封存類實(shí)例 for details).

嘗試封存不能被封存的對象會拋出 PicklingError 異常,異常發(fā)生時(shí),可能有部分字節(jié)已經(jīng)被寫入指定文件中。嘗試封存遞歸層級很深的對象時(shí),可能會超出最大遞歸層級限制,此時(shí)會拋出 RecursionError 異常,可以通過 sys.setrecursionlimit() 調(diào)整遞歸層級,不過請謹(jǐn)慎使用這個(gè)函數(shù),因?yàn)榭赡軙?dǎo)致解釋器崩潰。

Note that functions (built-in and user-defined) are pickled by fully qualified name, not by value. 2 This means that only the function name is pickled, along with the name of the containing module and classes. Neither the function's code, nor any of its function attributes are pickled. Thus the defining module must be importable in the unpickling environment, and the module must contain the named object, otherwise an exception will be raised. 3

Similarly, classes are pickled by fully qualified name, so the same restrictions in the unpickling environment apply. Note that none of the class's code or data is pickled, so in the following example the class attribute attr is not restored in the unpickling environment:

class Foo:
    attr = 'A class attribute'

picklestring = pickle.dumps(Foo)

These restrictions are why picklable functions and classes must be defined at the top level of a module.

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

封存類實(shí)例?

在本節(jié)中,我們描述了可用于定義、自定義和控制如何封存和解封類實(shí)例的通用流程。

通常,使一個(gè)實(shí)例可被封存不需要附加任何代碼。Pickle 默認(rèn)會通過 Python 的內(nèi)省機(jī)制獲得實(shí)例的類及屬性。而當(dāng)實(shí)例解封時(shí),它的 __init__() 方法通常 不會 被調(diào)用。其默認(rèn)動作是:先創(chuàng)建一個(gè)未初始化的實(shí)例,然后還原其屬性,下面的代碼展示了這種行為的實(shí)現(xiàn)機(jī)制:

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

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

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

object.__getnewargs_ex__()?

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

如果類的 __new__() 方法只接受關(guān)鍵字參數(shù),則應(yīng)當(dāng)實(shí)現(xiàn)這個(gè)方法。否則,為了兼容性,更推薦實(shí)現(xiàn) __getnewargs__() 方法。

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

object.__getnewargs__()?

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

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

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

object.__getstate__()?

Classes can further influence how their instances are pickled by overriding the method __getstate__(). It is called and the returned object is pickled as the contents for the instance, instead of a default state. There are several cases:

  • For a class that has no instance __dict__ and no __slots__, the default state is None.

  • For a class that has an instance __dict__ and no __slots__, the default state is self.__dict__.

  • For a class that has an instance __dict__ and __slots__, the default state is a tuple consisting of two dictionaries: self.__dict__, and a dictionary mapping slot names to slot values. Only slots that have a value are included in the latter.

  • For a class that has __slots__ and no instance __dict__, the default state is a tuple whose first item is None and whose second item is a dictionary mapping slot names to slot values described in the previous bullet.

在 3.11 版更改: Added the default implementation of the __getstate__() method in the object class.

object.__setstate__(state)?

當(dāng)解封時(shí),如果類定義了 __setstate__(),就會在已解封狀態(tài)下調(diào)用它。此時(shí)不要求實(shí)例的 state 對象必須是 dict。沒有定義此方法的話,先前封存的 state 對象必須是 dict,且該 dict 內(nèi)容會在解封時(shí)賦給新實(shí)例的 __dict__。

備注

如果 __getstate__() 返回 False,那么在解封時(shí)就不會調(diào)用 __setstate__() 方法。

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

備注

在解封時(shí),實(shí)例的某些方法例如 __getattr__(), __getattribute__()__setattr__() 可能會被調(diào)用。 由于這些方法可能要求某些內(nèi)部不變量為真值,因此該類型應(yīng)當(dāng)實(shí)現(xiàn) __new__() 以建立這樣的不變量,因?yàn)楫?dāng)解封一個(gè)實(shí)例時(shí) __init__() 并不會被調(diào)用。

可以看出,其實(shí) pickle 并不直接調(diào)用上面的幾個(gè)函數(shù)。事實(shí)上,這幾個(gè)函數(shù)是復(fù)制協(xié)議的一部分,它們實(shí)現(xiàn)了 __reduce__() 這一特殊接口。復(fù)制協(xié)議提供了統(tǒng)一的接口,用于在封存或復(fù)制對象的過程中取得所需數(shù)據(jù)。4

盡管這個(gè)協(xié)議功能很強(qiáng),但是直接在類中實(shí)現(xiàn) __reduce__() 接口容易產(chǎn)生錯(cuò)誤。因此,設(shè)計(jì)類時(shí)應(yīng)當(dāng)盡可能的使用高級接口(比如 __getnewargs_ex__()、__getstate__()__setstate__())。后面仍然可以看到直接實(shí)現(xiàn) __reduce__() 接口的狀況,可能別無他法,可能為了獲得更好的性能,或者兩者皆有之。

object.__reduce__()?

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

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

如果返回的是元組,則應(yīng)當(dāng)包含 2 到 6 個(gè)元素,可選元素可以省略或設(shè)置為 None。每個(gè)元素代表的意義如下:

  • 一個(gè)可調(diào)用對象,該對象會在創(chuàng)建對象的最初版本時(shí)調(diào)用。

  • 可調(diào)用對象的參數(shù),是一個(gè)元組。如果可調(diào)用對象不接受參數(shù),必須提供一個(gè)空元組。

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

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

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

  • 可選元素,一個(gè)帶有 (obj, state) 簽名的可調(diào)用對象。該可調(diào)用對象允許用戶以編程方式控制特定對象的狀態(tài)更新行為,而不是使用 obj 的靜態(tài) __setstate__() 方法。如果此處不是 None,則此可調(diào)用對象的優(yōu)先級高于 obj__setstate__()。

    3.8 新版功能: 新增了元組的第 6 項(xiàng),可選元素 (obj, state)。

object.__reduce_ex__(protocol)?

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

持久化外部對象?

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

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

要通過持久化 ID 將外部對象封存,必須在 pickler 中實(shí)現(xiàn) persistent_id() 方法,該方法接受需要被封存的對象作為參數(shù),返回一個(gè) None 或返回該對象的持久化 ID。如果返回 None,該對象會被按照默認(rèn)方式封存為數(shù)據(jù)流。如果返回字符串形式的持久化 ID,則會封存這個(gè)字符串并加上一個(gè)標(biāo)記,這樣 unpickler 才能將其識別為持久化 ID。

要解封外部對象,Unpickler 必須實(shí)現(xiàn) persistent_load() 方法,接受一個(gè)持久化 ID 對象作為參數(shù)并返回一個(gè)引用的對象。

下面是一個(gè)全面的例子,展示了如何使用持久化 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 表?

如果想對某些類進(jìn)行自定義封存,而又不想在類中增加用于封存的代碼,就可以創(chuàng)建帶有特殊 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

創(chuàng)建了一個(gè)帶有自有 dispatch 表的 pickle.Pickler 實(shí)例,它可以對 SomeClass 類進(jìn)行特殊處理。另外,下列代碼

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

does the same but all instances of MyPickler will by default share the private dispatch table. On the other hand, the code

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

modifies the global dispatch table shared by all users of the copyreg module.

處理有狀態(tài)的對象?

下面的示例展示了如何修改類在封存時(shí)的行為。其中 TextReader 類打開了一個(gè)文本文件,每次調(diào)用其 readline() 方法則返回行號和該行的字符。 在封存這個(gè) TextReader 的實(shí)例時(shí),除了 文件對象,其他屬性都會被保存。 當(dāng)解封實(shí)例時(shí),需要重新打開文件,然后從上次的位置開始繼續(xù)讀取。實(shí)現(xiàn)這些功能需要實(shí)現(xiàn) __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!'

類型,函數(shù)和其他對象的自定義歸約?

3.8 新版功能.

有時(shí),dispatch_table 可能不夠靈活。 特別是當(dāng)我們想要基于對象類型以外的其他規(guī)則來對封存進(jìn)行定制,或是當(dāng)我們想要對函數(shù)和類的封存進(jìn)行定制的時(shí)候。

對于那些情況,可能要基于 Pickler 類進(jìn)行子類化并實(shí)現(xiàn) reducer_override() 方法。 此方法可返回任意的歸約元組 (參見 __reduce__())。 它也可以選擇返回 NotImplemented 來回退到傳統(tǒng)行為。

如果同時(shí)定義了 dispatch_tablereducer_override(),則 reducer_override() 方法具有優(yōu)先權(quán)。

備注

出于性能理由,可能不會為以下對象調(diào)用 reducer_override(): None, True, False, 以及 int, float, bytes, str, dict, set, frozenset, listtuple 的具體實(shí)例。

以下是一個(gè)簡單的例子,其中我們允許封存并重新構(gòu)建一個(gè)給定的類:

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

外部緩沖區(qū)?

3.8 新版功能.

在某些場景中,pickle 模塊會被用來傳輸海量的數(shù)據(jù)。 因此,最小化內(nèi)存復(fù)制次數(shù)以保證性能和節(jié)省資源是很重要的。 但是 pickle 模塊的正常運(yùn)作會將圖類對象結(jié)構(gòu)轉(zhuǎn)換為字節(jié)序列流,因此在本質(zhì)上就要從封存流中來回復(fù)制數(shù)據(jù)。

如果 provider (待傳輸對象類型的實(shí)現(xiàn)) 和 consumer (通信系統(tǒng)的實(shí)現(xiàn)) 都支持 pickle 第 5 版或更高版本所提供的外部傳輸功能,則此約束可以被撤銷。

提供方 API?

大的待封存數(shù)據(jù)對象必須實(shí)現(xiàn)協(xié)議 5 及以上版本專屬的 __reduce_ex__() 方法,該方法將為任意大的數(shù)據(jù)返回一個(gè) PickleBuffer 實(shí)例(而不是 bytes 對象等)。

PickleBuffer 對象會 表明 底層緩沖區(qū)可被用于外部數(shù)據(jù)傳輸。 那些對象仍將保持與 pickle 模塊的正常用法兼容。 但是,使用方也可以選擇告知 pickle 它們將自行處理那些緩沖區(qū)。

使用方 API?

當(dāng)序列化一個(gè)對象圖時(shí),通信系統(tǒng)可以啟用對所生成 PickleBuffer 對象的定制處理。

發(fā)送端需要傳遞 buffer_callback 參數(shù)到 Pickler (或是到 dump()dumps() 函數(shù)),該回調(diào)函數(shù)將在封存對象圖時(shí)附帶每個(gè)所生成的 PickleBuffer 被調(diào)用。 由 buffer_callback 所累積的緩沖區(qū)的數(shù)據(jù)將不會被拷貝到 pickle 流,而是僅插入一個(gè)簡單的標(biāo)記。

接收端需要傳遞 buffers 參數(shù)到 Unpickler (或是到 load()loads() 函數(shù)),其值是一個(gè)由緩沖區(qū)組成的可迭代對象,它會被傳遞給 buffer_callback。 該可迭代對象應(yīng)當(dāng)按其被傳遞給 buffer_callback 時(shí)的順序產(chǎn)生緩沖區(qū)。 這些緩沖區(qū)將提供對象重構(gòu)造器所期望的數(shù)據(jù),對這些數(shù)據(jù)的封存產(chǎn)生了原本的 PickleBuffer 對象。

在發(fā)送端和接受端之間,通信系統(tǒng)可以自由地實(shí)現(xiàn)它自己用于外部緩沖區(qū)的傳輸機(jī)制。 潛在的優(yōu)化包括使用共享內(nèi)存或基于特定數(shù)據(jù)類型的壓縮等。

示例?

下面是一個(gè)小例子,在其中我們實(shí)現(xiàn)了一個(gè) bytearray 的子類,能夠用于外部緩沖區(qū)封存:

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)

重構(gòu)造器 (_reconstruct 類方法) 會在緩沖區(qū)的提供對象具有正確類型時(shí)返回該對象。 在此小示例中這是模擬零拷貝行為的便捷方式。

在使用方,我們可以按通常方式封存那些對象,它們在反序列化時(shí)將提供原始對象的一個(gè)副本:

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 然后在反序列化時(shí)給回累積的緩沖區(qū),我們就能夠取回原始對象:

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

這個(gè)例子受限于 bytearray 會自行分配內(nèi)存這一事實(shí):你無法基于另一個(gè)對象的內(nèi)存創(chuàng)建 bytearray 的實(shí)例。 但是,第三方數(shù)據(jù)類型例如 NumPy 數(shù)組則沒有這種限制,允許在單獨(dú)進(jìn)程或系統(tǒng)間傳輸時(shí)使用零拷貝的封存(或是盡可能少地拷貝) 。

參見

PEP 574 -- 帶有外部數(shù)據(jù)緩沖區(qū)的 pickle 協(xié)議 5

限制全局變量?

默認(rèn)情況下,解封將會導(dǎo)入在 pickle 數(shù)據(jù)中找到的任何類或函數(shù)。 對于許多應(yīng)用來說,此行為是不可接受的,因?yàn)樗鼤试S解封器導(dǎo)入并發(fā)起調(diào)用任意代碼。 只須考慮當(dāng)這個(gè)手工構(gòu)建的 pickle 數(shù)據(jù)流被加載時(shí)會做什么:

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

在這個(gè)例子里,解封器導(dǎo)入 os.system() 函數(shù)然后應(yīng)用字符串參數(shù) "echo hello world"。 雖然這個(gè)例子不具攻擊性,但是不難想象別人能夠通過此方式對你的系統(tǒng)造成損害。

出于這樣的理由,你可能會希望通過定制 Unpickler.find_class() 來控制要解封的對象。 與其名稱所提示的不同,Unpickler.find_class() 會在執(zhí)行對任何全局對象(例如一個(gè)類或一個(gè)函數(shù))的請求時(shí)被調(diào)用。 因此可以完全禁止全局對象或是將它們限制在一個(gè)安全的子集中。

下面的例子是一個(gè)解封器,它只允許某一些安全的來自 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()

A sample usage of our unpickler working as intended:

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

正如我們這個(gè)例子所顯示的,對于允許解封的對象你必須要保持謹(jǐn)慎。 因此如果要保證安全,你可以考慮其他選擇例如 xmlrpc.client 中的編組 API 或是第三方解決方案。

性能?

較新版本的 pickle 協(xié)議(第 2 版或更高)具有針對某些常見特性和內(nèi)置類型的高效二進(jìn)制編碼格式。 此外,pickle 模塊還擁有一個(gè)以 C 編寫的透明優(yōu)化器。

例子?

對于最簡單的代碼,請使用 dump()load() 函數(shù)。

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)

以下示例讀取之前封存的數(shù)據(jù)。

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

為擴(kuò)展類型提供 pickle 接口所需的構(gòu)造函數(shù)。

模塊 pickletools

用于處理和分析已封存數(shù)據(jù)的工具。

模塊 shelve

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

模塊 copy

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

模塊 marshal

高效地序列化內(nèi)置類型的數(shù)據(jù)。

備注

1

不要把它與 marshal 模塊混淆。

2

這就是為什么 lambda 函數(shù)不可以被封存:所有的匿名函數(shù)都有同一個(gè)名字:<lambda>

3

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

4

copy 模塊使用這一協(xié)議實(shí)現(xiàn)淺層 (shallow) 和深層 (deep) 復(fù)制操作。

5

The limitation on alphanumeric characters is due to the fact that persistent IDs in protocol 0 are delimited by the newline character. Therefore if any kind of newline characters occurs in persistent IDs, the resulting pickled data will become unreadable.