pickle —— Python 對(duì)象序列化?

源代碼: Lib/pickle.py


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

警告

pickle 模塊在接受被錯(cuò)誤地構(gòu)造或者被惡意地構(gòu)造的數(shù)據(jù)時(shí)不安全。永遠(yuǎn)不要 unpickle 來自于不受信任的或者未經(jīng)驗(yàn)證的來源的數(shù)據(jù)。

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

marshal 間的關(guān)系?

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

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

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

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

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

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

json 模塊的比較?

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

  • JSON 是一個(gè)文本序列化格式(它輸出 unicode 文本,盡管在大多數(shù)時(shí)候它會(huì)接著以 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 的對(duì)象內(nèi)省功能自動(dòng)地表示大多數(shù)類型,復(fù)雜情況可以通過實(shí)現(xiàn) specific object APIs 來解決)。

參見

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 對(duì)象。

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

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

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

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

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

  • v2 版協(xié)議是在 Python 2.3 中引入的。它為存儲(chǔ) new-style class 提供了更高效的機(jī)制。欲了解有關(guān)第 2 版協(xié)議帶來的改進(jìn),請(qǐng)參閱 PEP 307。

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

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

注解

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

模塊接口?

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

pickle 模塊包含了以下常量:

pickle.HIGHEST_PROTOCOL?

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

pickle.DEFAULT_PROTOCOL?

一個(gè)整數(shù),表示封存操作使用的 協(xié)議版本。 它可能小于 HIGHEST_PROTOCOL。 當(dāng)前默認(rèn)協(xié)議版本為 3,它是一個(gè)為 Python 3 設(shè)計(jì)的新協(xié)議。

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

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

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

可選參數(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í)例,也可以是滿足這一接口的其他任何自定義對(duì)象。

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

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

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

參數(shù) protocolfix_imports 的含義與它們?cè)?dump() 中的含義相同。

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

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

Pickle 協(xié)議版本是自動(dòng)檢測(cè)出來的,所以不需要參數(shù)來指定協(xié)議。打包對(duì)象以外的其他字節(jié)將被忽略。

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

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

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

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

Pickle 協(xié)議版本是自動(dòng)檢測(cè)出來的,所以不需要參數(shù)來指定協(xié)議。打包對(duì)象以外的其他字節(jié)將被忽略。

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

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

exception pickle.PickleError?

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

exception pickle.PicklingError?

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

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

exception pickle.UnpicklingError?

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

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

pickle 模塊可導(dǎo)出兩個(gè)類,PicklerUnpickler:

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

它接受一個(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í)例,也可以是滿足這一接口的其他任何自定義對(duì)象。

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

dump(obj)?

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

persistent_id(obj)?

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

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

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

dispatch_table?

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

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

參閱 Dispatch 表 獲取使用示例。

3.3 新版功能.

fast?

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

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

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

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

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

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

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

load()?

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

persistent_load(pid)?

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

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

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

find_class(module, name)?

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

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

可以被打包/解包的對(duì)象?

下列類型可以被打包:

  • None、TrueFalse

  • 整數(shù)、浮點(diǎn)數(shù)、復(fù)數(shù)

  • str、byte、bytearray

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

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

  • 定義在模塊頂層的內(nèi)置函數(shù)

  • 定義在模塊頂層的類

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

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

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

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

class Foo:
    attr = 'A class attribute'

picklestring = pickle.dumps(Foo)

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

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

打包類實(shí)例?

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

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

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

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

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

object.__getnewargs_ex__()?

對(duì)于使用第 2 版或更高版協(xié)議的 pickle,實(shí)現(xiàn)了 __getnewargs_ex__() 方法的類可以控制在解包時(shí)傳給 __new__() 方法的參數(shù)。本方法必須返回一對(duì) (args, kwargs) 用于構(gòu)建對(duì)象,其中 args 是表示位置參數(shù)的 tuple,而 kwargs 是表示命名參數(shù)的 dict。它們會(huì)在解包時(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__() 就不會(huì)被調(diào)用。

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

object.__getstate__()?

類還可以進(jìn)一步控制其實(shí)例的打包過程。如果類定義了 __getstate__(),它就會(huì)被調(diào)用,其返回的對(duì)象是被當(dāng)做實(shí)例內(nèi)容來打包的,否則打包的是實(shí)例的 __dict__。如果 __getstate__() 未定義,實(shí)例的 __dict__ 會(huì)被照常打包。

object.__setstate__(state)?

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

注解

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

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

注解

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

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

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

object.__reduce__()?

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

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

當(dāng)返回的是一個(gè)元組時(shí),它的長(zhǎng)度必須在二至五項(xiàng)之間。 可選項(xiàng)可以被省略或?qū)⒅翟O(shè)為 None。 每項(xiàng)的語義分別如下所示:

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

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

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

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

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

object.__reduce_ex__(protocol)?

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

持久化外部對(duì)象?

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

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

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

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

下面是一個(gè)全面的例子,展示了如何使用持久化 ID 來封存外部對(duì)象。

# 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 表?

如果想對(duì)某些類進(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í)例,它可以對(duì) SomeClass 類進(jìn)行特殊處理。另外,下列代碼:

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

完成了相同的操作,但所有 MyPickler 的實(shí)例都會(huì)共用同一份 dispatch 表。使用 copyreg 模塊實(shí)現(xiàn)的等效代碼是:

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

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

下面的示例展示了如何修改類在封存時(shí)的行為。其中 TextReader 類打開了一個(gè)文本文件,每次調(diào)用其 readline() 方法則返回行號(hào)和該行的字符。 在封存這個(gè) TextReader 的實(shí)例時(shí),除了 文件對(duì)象,其他屬性都會(huì)被保存。 當(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!'

限制全局變量?

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

>>> 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è)例子不具攻擊性,但是不難想象別人能夠通過此方式對(duì)你的系統(tǒng)造成損害。

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

我們這個(gè)解封器的一個(gè)示例用法所達(dá)成的目標(biāo):

>>> 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è)例子所顯示的,對(duì)于允許解封的對(duì)象你必須要保持謹(jǐn)慎。 因此如果要保證安全,你可以考慮其他選擇例如 xmlrpc.client 中的編組 API 或是第三方解決方案。

性能?

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

例子?

對(duì)于最簡(jiǎn)單的代碼,請(qǐng)使用 dump()load() 函數(shù)。

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)

以下示例讀取之前封存的數(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ù)庫,用于存放對(duì)象,使用了 pickle 模塊。

模塊 copy

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

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

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