pickle
--- Python 對象序列化?
源代碼: Lib/pickle.py
模塊 pickle
實現(xiàn)了對一個 Python 對象結(jié)構(gòu)的二進制序列化和反序列化。 "pickling" 是將 Python 對象及其所擁有的層次結(jié)構(gòu)轉(zhuǎn)化為一個字節(jié)流的過程,而 "unpickling" 是相反的操作,會將(來自一個 binary file 或者 bytes-like object 的)字節(jié)流轉(zhuǎn)化回一個對象層次結(jié)構(gòu)。 pickling(和 unpickling)也被稱為“序列化”, “編組” 1 或者 “平面化”。而為了避免混亂,此處采用術(shù)語 “封存 (pickling)” 和 “解封 (unpickling)”。
警告
pickle
模塊**并不安全**。你只應(yīng)該對你信任的數(shù)據(jù)進行unpickle操作。
構(gòu)建惡意的 pickle 數(shù)據(jù)來**在解封時執(zhí)行任意代碼**是可能的。絕對不要對不信任來源的數(shù)據(jù)和可能被篡改過的數(shù)據(jù)進行解封。
請考慮使用 hmac
來對數(shù)據(jù)進行簽名,確保數(shù)據(jù)沒有被篡改。
在你處理不信任數(shù)據(jù)時,更安全的序列化格式如 json
可能更為適合。參見 與 json 模塊的比較 。
與其他 Python 模塊間的關(guān)系?
與 marshal
間的關(guān)系?
Python 有一個更原始的序列化模塊稱為 marshal
,但一般地 pickle
應(yīng)該是序列化 Python 對象時的首選。marshal
存在主要是為了支持 Python 的 .pyc
文件.
pickle
模塊與 marshal
在如下幾方面顯著地不同:
pickle
模塊會跟蹤已被序列化的對象,所以該對象之后再次被引用時不會再次被序列化。marshal
不會這么做。這隱含了遞歸對象和共享對象。遞歸對象指包含對自己的引用的對象。這種對象并不會被 marshal 接受,并且實際上嘗試 marshal 遞歸對象會讓你的 Python 解釋器崩潰。對象共享發(fā)生在對象層級中存在多處引用同一對象時。
pickle
只會存儲這些對象一次,并確保其他的引用指向同一個主副本。共享對象將保持共享,這可能對可變對象非常重要。marshal
不能被用于序列化用戶定義類及其實例。pickle
能夠透明地存儲并保存類實例,然而此時類定義必須能夠從與被存儲時相同的模塊被引入。同樣用于序列化的
marshal
格式不保證數(shù)據(jù)能移植到不同的 Python 版本中。因為它的主要任務(wù)是支持.pyc
文件,必要時會以破壞向后兼容的方式更改這種序列化格式,為此 Python 的實現(xiàn)者保留了更改格式的權(quán)利。pickle
序列化格式可以在不同版本的 Python 中實現(xiàn)向后兼容,前提是選擇了合適的 pickle 協(xié)議。如果你的數(shù)據(jù)要在 Python 2 與 Python 3 之間跨越傳遞,封存和解封的代碼在 2 和 3 之間也是不同的。
與 json
模塊的比較?
Pickle 協(xié)議和 JSON (JavaScript Object Notation) 間有著本質(zhì)的不同:
JSON 是一個文本序列化格式(它輸出 unicode 文本,盡管在大多數(shù)時候它會接著以
utf-8
編碼),而 pickle 是一個二進制序列化格式;JSON 是我們可以直觀閱讀的,而 pickle 不是;
JSON是可互操作的,在Python系統(tǒng)之外廣泛使用,而pickle則是Python專用的;
默認情況下,JSON 只能表示 Python 內(nèi)置類型的子集,不能表示自定義的類;但 pickle 可以表示大量的 Python 數(shù)據(jù)類型(可以合理使用 Python 的對象內(nèi)省功能自動地表示大多數(shù)類型,復雜情況可以通過實現(xiàn) specific object APIs 來解決)。
不像pickle,對一個不信任的JSON進行反序列化的操作本身不會造成任意代碼執(zhí)行漏洞。
參見
json
模塊:一個允許JSON序列化和反序列化的標準庫模塊
數(shù)據(jù)流格式?
pickle
所使用的數(shù)據(jù)格式僅可用于 Python。這樣做的好處是沒有外部標準給該格式強加限制,比如 JSON 或 XDR(不能表示共享指針)標準;但這也意味著非 Python 程序可能無法重新讀取 pickle 封存的 Python 對象。
默認情況下,pickle
格式使用相對緊湊的二進制來存儲。如果需要讓文件更小,可以高效地 壓縮 由 pickle 封存的數(shù)據(jù)。
pickletools
模塊包含了相應(yīng)的工具用于分析 pickle
生成的數(shù)據(jù)流。pickletools
源碼中包含了對 pickle 協(xié)議使用的操作碼的大量注釋。
當前共有 6 種不同的協(xié)議可用于封存操作。 使用的協(xié)議版本越高,讀取所生成 pickle 對象所需的 Python 版本就要越新。
v0 版協(xié)議是原始的“人類可讀”協(xié)議,并且向后兼容早期版本的 Python。
v1 版協(xié)議是較早的二進制格式,它也與早期版本的 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 的默認協(xié)議。v4 版協(xié)議添加于 Python 3.4。它支持存儲非常大的對象,能存儲更多種類的對象,還包括一些針對數(shù)據(jù)格式的優(yōu)化。它是Python 3.8使用的默認協(xié)議。有關(guān)第 4 版協(xié)議帶來改進的信息,請參閱 PEP 3154。
第 5 版協(xié)議是在 Python 3.8 中加入的。 它增加了對帶外數(shù)據(jù)的支持,并可加速帶內(nèi)數(shù)據(jù)處理。 請參閱 PEP 574 了解第 5 版協(xié)議所帶來的改進的詳情。
模塊接口?
要序列化某個包含層次結(jié)構(gòu)的對象,只需調(diào)用 dumps()
函數(shù)即可。同樣,要反序列化數(shù)據(jù)流,可以調(diào)用 loads()
函數(shù)。但是,如果要對序列化和反序列化加以更多的控制,可以分別創(chuàng)建 Pickler
或 Unpickler
對象。
pickle
模塊包含了以下常量:
- pickle.HIGHEST_PROTOCOL?
整數(shù),可用的最高 協(xié)議版本。此值可以作為 協(xié)議 值傳遞給
dump()
和dumps()
函數(shù),以及Pickler
的構(gòu)造函數(shù)。
- pickle.DEFAULT_PROTOCOL?
整數(shù),用于 pickle 數(shù)據(jù)的默認 協(xié)議版本。它可能小于
HIGHEST_PROTOCOL
。當前默認協(xié)議是 v4,它在 Python 3.4 中首次引入,與之前的版本不兼容。在 3.0 版更改: 默認協(xié)議版本是 3。
在 3.8 版更改: 默認協(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_imports 和 buffer_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_imports 和 buffer_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)并返回。它相當于
Unpickler(file).load()
。Pickle 協(xié)議版本是自動檢測出來的,所以不需要參數(shù)來指定協(xié)議。封存對象以外的其他字節(jié)將被忽略。
參數(shù) file、fix_imports、encoding、errors、strict 和 buffers 的含義與它們在
Unpickler
的構(gòu)造函數(shù)中的含義相同。在 3.8 版更改: 加入了 buffers 參數(shù)。
- pickle.loads(data, /, *, fix_imports=True, encoding='ASCII', errors='strict', buffers=None)?
重建并返回一個對象的封存表示形式 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 個異常:
- exception pickle.PicklingError?
當
Pickler
遇到無法解封的對象時拋出此錯誤。它是PickleError
的子類。參考 可以被封存/解封的對象 來了解哪些對象可以被封存。
- exception pickle.UnpicklingError?
當解封出錯時拋出此異常,例如數(shù)據(jù)損壞或?qū)ο蟛话踩?。它?
PickleError
的子類。注意,解封時可能還會拋出其他異常,包括(但不限于) AttributeError、EOFError、ImportError 和 IndexError。
pickle
模塊包含了 3 個類,Pickler
、Unpickler
和 PickleBuffer
:
- class pickle.Pickler(file, protocol=None, *, fix_imports=True, buffer_callback=None)?
它接受一個二進制文件用于寫入 pickle 數(shù)據(jù)流。
可選參數(shù) protocol 是一個整數(shù),告知 pickler 使用指定的協(xié)議,可選擇的協(xié)議范圍從 0 到
HIGHEST_PROTOCOL
。如果沒有指定,這一參數(shù)默認值為DEFAULT_PROTOCOL
。指定一個負數(shù)就相當于指定HIGHEST_PROTOCOL
。參數(shù) file 必須有一個 write() 方法,該 write() 方法要能接收字節(jié)作為其唯一參數(shù)。因此,它可以是一個打開的磁盤文件(用于寫入二進制內(nèi)容),也可以是一個
io.BytesIO
實例,也可以是滿足這一接口的其他任何自定義對象。如果 fix_imports 為 True 且 protocol 小于 3,pickle 將嘗試將 Python 3 中的新名稱映射到 Python 2 中的舊模塊名稱,因此 Python 2 也可以讀取封存的數(shù)據(jù)流。
如果 buffer_callback 為 None(默認情況),緩沖區(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,就會出錯。
在 3.8 版更改: 加入了 buffer_callback 參數(shù)。
- dump(obj)?
將 obj 封存后的內(nèi)容寫入已打開的文件對象,該文件對象已經(jīng)在構(gòu)造函數(shù)中指定。
- persistent_id(obj)?
默認無動作,子類繼承重載時使用。
如果
persistent_id()
返回None
,obj 會被照常 pickle。如果返回其他值,Pickler
會將這個函數(shù)的返回值作為 obj 的持久化 ID(Pickler 本應(yīng)得到序列化數(shù)據(jù)流并將其寫入文件,若此函數(shù)有返回值,則得到此函數(shù)的返回值并寫入文件)。這個持久化 ID 的解釋應(yīng)當定義在Unpickler.persistent_load()
中(該方法定義還原對象的過程,并返回得到的對象)。注意,persistent_id()
的返回值本身不能擁有持久化 ID。參閱 持久化外部對象 獲取詳情和使用示例。
- dispatch_table?
Pickler 對象的 dispatch 表是
copyreg.pickle()
中用到的 reduction 函數(shù) 的注冊。dispatch 表本身是一個 class 到其 reduction 函數(shù)的映射鍵值對。一個 reduction 函數(shù)只接受一個參數(shù),就是其關(guān)聯(lián)的 class,函數(shù)行為應(yīng)當遵守__reduce__()
接口規(guī)范。Pickler 對象默認并沒有
dispatch_table
屬性,該對象默認使用copyreg
模塊中定義的全局 dispatch 表。如果要為特定 Pickler 對象自定義序列化過程,可以將dispatch_table
屬性設(shè)置為類字典對象(dict-like object)。另外,如果Pickler
的子類設(shè)置了dispatch_table
屬性,則該子類的實例會使用這個表作為默認的 dispatch 表。參閱 Dispatch 表 獲取使用示例。
3.3 新版功能.
- reducer_override(obj)?
可以在
Pickler
的子類中定義的特殊 reducer。此方法的優(yōu)先級高于dispatch_table
中的任何 reducer。它應(yīng)該與__reduce__()
方法遵循相同的接口,它也可以返回NotImplemented
,這將使用dispatch_table
里注冊的 reducer 來封存obj
。參閱 類型,函數(shù)和其他對象的自定義歸約 獲取詳細的示例。
3.8 新版功能.
- fast?
已棄用。設(shè)為 True 則啟用快速模式??焖倌J浇昧恕皞渫洝?(memo) 的使用,即不生成多余的 PUT 操作碼來加快封存過程。不應(yīng)將其與自指 (self-referential) 對象一起使用,否則將導致
Pickler
無限遞歸。如果需要進一步提高 pickle 的壓縮率,請使用
pickletools.optimize()
。
- class pickle.Unpickler(file, *, fix_imports=True, encoding='ASCII', errors='strict', buffers=None)?
它接受一個二進制文件用于讀取 pickle 數(shù)據(jù)流。
Pickle 協(xié)議版本是自動檢測出來的,所以不需要參數(shù)來指定協(xié)議。
參數(shù) file 必須有三個方法,read() 方法接受一個整數(shù)參數(shù),readinto() 方法接受一個緩沖區(qū)作為參數(shù),readline() 方法不需要參數(shù),這與
io.BufferedIOBase
里定義的接口是相同的。因此 file 可以是一個磁盤上用于二進制讀取的文件,也可以是一個io.BytesIO
實例,也可以是滿足這一接口的其他任何自定義對象。可選的參數(shù)是 fix_imports, encoding 和 errors,用于控制由Python 2 生成的 pickle 流的兼容性。如果 fix_imports 為 True,則 pickle 將嘗試將舊的 Python 2 名稱映射到 Python 3 中對應(yīng)的新名稱。encoding 和 errors 參數(shù)告訴 pickle 如何解碼 Python 2 存儲的 8 位字符串實例;這兩個參數(shù)默認分別為 'ASCII' 和 'strict'。encoding 參數(shù)可置為 'bytes' 來將這些 8 位字符串實例讀取為字節(jié)對象。讀取 NumPy array 和 Python 2 存儲的
datetime
、date
和time
實例時,請使用encoding='latin1'
。如果 buffers 為 None(默認值),則反序列化所需的所有數(shù)據(jù)都必須包含在 pickle 流中。這意味著在實例化
Pickler
時(或調(diào)用dump()
或dumps()
時),參數(shù) buffer_callback 為 None。如果 buffers 不為 None,則每次 pickle 流引用 帶外 緩沖區(qū)視圖時,消耗的對象都應(yīng)該是可迭代的啟用緩沖區(qū)的對象。這樣的緩沖區(qū)應(yīng)該按順序地提供給 Pickler 對象的 buffer_callback 方法。
在 3.8 版更改: 加入了 buffers 參數(shù)。
- load()?
從構(gòu)造函數(shù)中指定的文件對象里讀取封存好的對象,重建其中特定對象的層次結(jié)構(gòu)并返回。封存對象以外的其他字節(jié)將被忽略。
- persistent_load(pid)?
默認拋出
UnpicklingError
異常。如果定義了此方法,
persistent_load()
應(yīng)當返回持久化 ID pid 所指定的對象。 如果遇到無效的持久化 ID,則應(yīng)當引發(fā)UnpicklingError
。參閱 持久化外部對象 獲取詳情和使用示例。
- find_class(module, name)?
如有必要,導入 module 模塊并返回其中名叫 name 的對象,其中 module 和 name 參數(shù)都是
str
對象。注意,不要被這個函數(shù)的名字迷惑,find_class()
同樣可以用來導入函數(shù)。子類可以重載此方法,來控制加載對象的類型和加載對象的方式,從而盡可能降低安全風險。參閱 限制全局變量 獲取更詳細的信息。
引發(fā)一個 審計事件
pickle.find_class
附帶參數(shù)module
、name
。
- class pickle.PickleBuffer(buffer)?
緩沖區(qū)的包裝器 (wrapper),緩沖區(qū)中包含著可封存的數(shù)據(jù)。buffer 必須是一個 buffer-providing 對象,比如 bytes-like object 或多維數(shù)組。
PickleBuffer
本身就可以生成緩沖區(qū)對象,因此可以將其傳遞給需要緩沖區(qū)生成器的其他 API,比如memoryview
。PickleBuffer
對象只能用 pickle 版本 5 及以上協(xié)議進行序列化。它們符合 帶外序列化 的條件。3.8 新版功能.
- raw()?
返回該緩沖區(qū)底層內(nèi)存區(qū)域的
memoryview
。 返回的對象是一維的、C 連續(xù)布局的 memoryview,格式為B
(無符號字節(jié))。 如果緩沖區(qū)既不是 C 連續(xù)布局也不是 Fortran 連續(xù)布局的,則拋出BufferError
異常。
- release()?
釋放由 PickleBuffer 占用的底層緩沖區(qū)。
可以被封存/解封的對象?
下列類型可以被封存:
None
,True
, andFalse
;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
, notlambda
);classes accessible from the top level of a module;
instances of such classes whose the result of calling
__getstate__()
is picklable (see section 封存類實例 for details).
嘗試封存不能被封存的對象會拋出 PicklingError
異常,異常發(fā)生時,可能有部分字節(jié)已經(jīng)被寫入指定文件中。嘗試封存遞歸層級很深的對象時,可能會超出最大遞歸層級限制,此時會拋出 RecursionError
異常,可以通過 sys.setrecursionlimit()
調(diào)整遞歸層級,不過請謹慎使用這個函數(shù),因為可能會導致解釋器崩潰。
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ù)據(jù)不會跟著實例一起被封存,只有實例數(shù)據(jù)會被封存。這樣設(shè)計是有目的的,在將來修復類中的錯誤、給類增加方法之后,仍然可以載入原來版本類實例的封存數(shù)據(jù)來還原該實例。如果你準備長期使用一個對象,可能會同時存在較多版本的類體,可以為對象添加版本號,這樣就可以通過類的 __setstate__()
方法將老版本轉(zhuǎn)換成新版本。
封存類實例?
在本節(jié)中,我們描述了可用于定義、自定義和控制如何封存和解封類實例的通用流程。
通常,使一個實例可被封存不需要附加任何代碼。Pickle 默認會通過 Python 的內(nèi)省機制獲得實例的類及屬性。而當實例解封時,它的 __init__()
方法通常 不會 被調(diào)用。其默認動作是:先創(chuàng)建一個未初始化的實例,然后還原其屬性,下面的代碼展示了這種行為的實現(xiàn)機制:
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 版或更高版協(xié)議的 pickle,實現(xiàn)了
__getnewargs_ex__()
方法的類可以控制在解封時傳給__new__()
方法的參數(shù)。本方法必須返回一對(args, kwargs)
用于構(gòu)建對象,其中 args 是表示位置參數(shù)的 tuple,而 kwargs 是表示命名參數(shù)的 dict。它們會在解封時傳遞給__new__()
方法。如果類的
__new__()
方法只接受關(guān)鍵字參數(shù),則應(yīng)當實現(xiàn)這個方法。否則,為了兼容性,更推薦實現(xiàn)__getnewargs__()
方法。在 3.6 版更改:
__getnewargs_ex__()
現(xiàn)在可用于第 2 和第 3 版協(xié)議。
- object.__getnewargs__()?
這個方法與上一個
__getnewargs_ex__()
方法類似,但僅支持位置參數(shù)。它要求返回一個 tuple 類型的args
,用于解封時傳遞給__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 isNone
.For a class that has an instance
__dict__
and no__slots__
, the default state isself.__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 isNone
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 theobject
class.
- object.__setstate__(state)?
當解封時,如果類定義了
__setstate__()
,就會在已解封狀態(tài)下調(diào)用它。此時不要求實例的 state 對象必須是 dict。沒有定義此方法的話,先前封存的 state 對象必須是 dict,且該 dict 內(nèi)容會在解封時賦給新實例的 __dict__。備注
如果
__getstate__()
返回 False,那么在解封時就不會調(diào)用__setstate__()
方法。
參考 處理有狀態(tài)的對象 一段獲取如何使用 __getstate__()
和 __setstate__()
方法的更多信息。
備注
在解封時,實例的某些方法例如 __getattr__()
, __getattribute__()
或 __setattr__()
可能會被調(diào)用。 由于這些方法可能要求某些內(nèi)部不變量為真值,因此該類型應(yīng)當實現(xiàn) __new__()
以建立這樣的不變量,因為當解封一個實例時 __init__()
并不會被調(diào)用。
可以看出,其實 pickle 并不直接調(diào)用上面的幾個函數(shù)。事實上,這幾個函數(shù)是復制協(xié)議的一部分,它們實現(xiàn)了 __reduce__()
這一特殊接口。復制協(xié)議提供了統(tǒng)一的接口,用于在封存或復制對象的過程中取得所需數(shù)據(jù)。4
盡管這個協(xié)議功能很強,但是直接在類中實現(xiàn) __reduce__()
接口容易產(chǎn)生錯誤。因此,設(shè)計類時應(yīng)當盡可能的使用高級接口(比如 __getnewargs_ex__()
、__getstate__()
和 __setstate__()
)。后面仍然可以看到直接實現(xiàn) __reduce__()
接口的狀況,可能別無他法,可能為了獲得更好的性能,或者兩者皆有之。
- object.__reduce__()?
該接口當前定義如下。
__reduce__()
方法不帶任何參數(shù),并且應(yīng)返回字符串或最好返回一個元組(返回的對象通常稱為“reduce 值”)。如果返回字符串,該字符串會被當做一個全局變量的名稱。它應(yīng)該是對象相對于其模塊的本地名稱,pickle 模塊會搜索模塊命名空間來確定對象所屬的模塊。這種行為常在單例模式使用。
如果返回的是元組,則應(yīng)當包含 2 到 6 個元素,可選元素可以省略或設(shè)置為
None
。每個元素代表的意義如下:一個可調(diào)用對象,該對象會在創(chuàng)建對象的最初版本時調(diào)用。
可調(diào)用對象的參數(shù),是一個元組。如果可調(diào)用對象不接受參數(shù),必須提供一個空元組。
可選元素,用于表示對象的狀態(tài),將被傳給前述的
__setstate__()
方法。 如果對象沒有此方法,則這個元素必須是字典類型,并會被添加至__dict__
屬性中。可選元素,一個返回連續(xù)項的迭代器(而不是序列)。這些項會被
obj.append(item)
逐個加入對象,或被obj.extend(list_of_items)
批量加入對象。這個元素主要用于 list 的子類,也可以用于那些正確實現(xiàn)了append()
和extend()
方法的類。(具體是使用append()
還是extend()
取決于 pickle 協(xié)議版本以及待插入元素的項數(shù),所以這兩個方法必須同時被類支持。)可選元素,一個返回連續(xù)鍵值對的迭代器(而不是序列)。這些鍵值對將會以
obj[key] = value
的方式存儲于對象中。該元素主要用于 dict 子類,也可以用于那些實現(xiàn)了__setitem__()
的類。可選元素,一個帶有
(obj, state)
簽名的可調(diào)用對象。該可調(diào)用對象允許用戶以編程方式控制特定對象的狀態(tài)更新行為,而不是使用obj
的靜態(tài)__setstate__()
方法。如果此處不是None
,則此可調(diào)用對象的優(yōu)先級高于obj
的__setstate__()
。3.8 新版功能: 新增了元組的第 6 項,可選元素
(obj, state)
。
- object.__reduce_ex__(protocol)?
作為替代選項,也可以實現(xiàn)
__reduce_ex__()
方法。 此方法的唯一不同之處在于它應(yīng)接受一個整型參數(shù)用于指定協(xié)議版本。 如果定義了這個函數(shù),則會覆蓋__reduce__()
的行為。 此外,__reduce__()
方法會自動成為擴展版方法的同義詞。 這個函數(shù)主要用于為以前的 Python 版本提供向后兼容的 reduce 值。
持久化外部對象?
為了獲取對象持久化的利益, pickle
模塊支持引用已封存數(shù)據(jù)流之外的對象。 這樣的對象是通過一個持久化 ID 來引用的,它應(yīng)當是一個由字母數(shù)字類字符組成的字符串 (對于第 0 版協(xié)議) 5 或是一個任意對象 (用于任意新版協(xié)議)。
pickle
模塊不提供對持久化 ID 的解析工作,它將解析工作分配給用戶定義的方法,分別是 pickler 中的 persistent_id()
方法和 unpickler 中的 persistent_load()
方法。
要通過持久化 ID 將外部對象封存,必須在 pickler 中實現(xiàn) persistent_id()
方法,該方法接受需要被封存的對象作為參數(shù),返回一個 None
或返回該對象的持久化 ID。如果返回 None
,該對象會被按照默認方式封存為數(shù)據(jù)流。如果返回字符串形式的持久化 ID,則會封存這個字符串并加上一個標記,這樣 unpickler 才能將其識別為持久化 ID。
要解封外部對象,Unpickler 必須實現(xiàn) persistent_load()
方法,接受一個持久化 ID 對象作為參數(shù)并返回一個引用的對象。
下面是一個全面的例子,展示了如何使用持久化 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 表?
如果想對某些類進行自定義封存,而又不想在類中增加用于封存的代碼,就可以創(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)建了一個帶有自有 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)
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)的對象?
下面的示例展示了如何修改類在封存時的行為。其中 TextReader
類打開了一個文本文件,每次調(diào)用其 readline()
方法則返回行號和該行的字符。 在封存這個 TextReader
的實例時,除了 文件對象,其他屬性都會被保存。 當解封實例時,需要重新打開文件,然后從上次的位置開始繼續(xù)讀取。實現(xiàn)這些功能需要實現(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 新版功能.
有時,dispatch_table
可能不夠靈活。 特別是當我們想要基于對象類型以外的其他規(guī)則來對封存進行定制,或是當我們想要對函數(shù)和類的封存進行定制的時候。
對于那些情況,可能要基于 Pickler
類進行子類化并實現(xiàn) reducer_override()
方法。 此方法可返回任意的歸約元組 (參見 __reduce__()
)。 它也可以選擇返回 NotImplemented
來回退到傳統(tǒng)行為。
如果同時定義了 dispatch_table
和 reducer_override()
,則 reducer_override()
方法具有優(yōu)先權(quán)。
備注
出于性能理由,可能不會為以下對象調(diào)用 reducer_override()
: None
, True
, False
, 以及 int
, float
, bytes
, str
, dict
, set
, frozenset
, list
和 tuple
的具體實例。
以下是一個簡單的例子,其中我們允許封存并重新構(gòu)建一個給定的類:
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)存復制次數(shù)以保證性能和節(jié)省資源是很重要的。 但是 pickle
模塊的正常運作會將圖類對象結(jié)構(gòu)轉(zhuǎn)換為字節(jié)序列流,因此在本質(zhì)上就要從封存流中來回復制數(shù)據(jù)。
如果 provider (待傳輸對象類型的實現(xiàn)) 和 consumer (通信系統(tǒng)的實現(xiàn)) 都支持 pickle 第 5 版或更高版本所提供的外部傳輸功能,則此約束可以被撤銷。
提供方 API?
大的待封存數(shù)據(jù)對象必須實現(xiàn)協(xié)議 5 及以上版本專屬的 __reduce_ex__()
方法,該方法將為任意大的數(shù)據(jù)返回一個 PickleBuffer
實例(而不是 bytes
對象等)。
PickleBuffer
對象會 表明 底層緩沖區(qū)可被用于外部數(shù)據(jù)傳輸。 那些對象仍將保持與 pickle
模塊的正常用法兼容。 但是,使用方也可以選擇告知 pickle
它們將自行處理那些緩沖區(qū)。
使用方 API?
當序列化一個對象圖時,通信系統(tǒng)可以啟用對所生成 PickleBuffer
對象的定制處理。
發(fā)送端需要傳遞 buffer_callback 參數(shù)到 Pickler
(或是到 dump()
或 dumps()
函數(shù)),該回調(diào)函數(shù)將在封存對象圖時附帶每個所生成的 PickleBuffer
被調(diào)用。 由 buffer_callback 所累積的緩沖區(qū)的數(shù)據(jù)將不會被拷貝到 pickle 流,而是僅插入一個簡單的標記。
接收端需要傳遞 buffers 參數(shù)到 Unpickler
(或是到 load()
或 loads()
函數(shù)),其值是一個由緩沖區(qū)組成的可迭代對象,它會被傳遞給 buffer_callback。 該可迭代對象應(yīng)當按其被傳遞給 buffer_callback 時的順序產(chǎn)生緩沖區(qū)。 這些緩沖區(qū)將提供對象重構(gòu)造器所期望的數(shù)據(jù),對這些數(shù)據(jù)的封存產(chǎn)生了原本的 PickleBuffer
對象。
在發(fā)送端和接受端之間,通信系統(tǒng)可以自由地實現(xiàn)它自己用于外部緩沖區(qū)的傳輸機制。 潛在的優(yōu)化包括使用共享內(nèi)存或基于特定數(shù)據(jù)類型的壓縮等。
示例?
下面是一個小例子,在其中我們實現(xiàn)了一個 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ū)的提供對象具有正確類型時返回該對象。 在此小示例中這是模擬零拷貝行為的便捷方式。
在使用方,我們可以按通常方式封存那些對象,它們在反序列化時將提供原始對象的一個副本:
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 然后在反序列化時給回累積的緩沖區(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
這個例子受限于 bytearray
會自行分配內(nèi)存這一事實:你無法基于另一個對象的內(nèi)存創(chuàng)建 bytearray
的實例。 但是,第三方數(shù)據(jù)類型例如 NumPy 數(shù)組則沒有這種限制,允許在單獨進程或系統(tǒng)間傳輸時使用零拷貝的封存(或是盡可能少地拷貝) 。
參見
PEP 574 -- 帶有外部數(shù)據(jù)緩沖區(qū)的 pickle 協(xié)議 5
限制全局變量?
默認情況下,解封將會導入在 pickle 數(shù)據(jù)中找到的任何類或函數(shù)。 對于許多應(yīng)用來說,此行為是不可接受的,因為它會允許解封器導入并發(fā)起調(diào)用任意代碼。 只須考慮當這個手工構(gòu)建的 pickle 數(shù)據(jù)流被加載時會做什么:
>>> import pickle
>>> pickle.loads(b"cos\nsystem\n(S'echo hello world'\ntR.")
hello world
0
在這個例子里,解封器導入 os.system()
函數(shù)然后應(yīng)用字符串參數(shù) "echo hello world"。 雖然這個例子不具攻擊性,但是不難想象別人能夠通過此方式對你的系統(tǒng)造成損害。
出于這樣的理由,你可能會希望通過定制 Unpickler.find_class()
來控制要解封的對象。 與其名稱所提示的不同,Unpickler.find_class()
會在執(zhí)行對任何全局對象(例如一個類或一個函數(shù))的請求時被調(diào)用。 因此可以完全禁止全局對象或是將它們限制在一個安全的子集中。
下面的例子是一個解封器,它只允許某一些安全的來自 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
正如我們這個例子所顯示的,對于允許解封的對象你必須要保持謹慎。 因此如果要保證安全,你可以考慮其他選擇例如 xmlrpc.client
中的編組 API 或是第三方解決方案。
性能?
較新版本的 pickle 協(xié)議(第 2 版或更高)具有針對某些常見特性和內(nèi)置類型的高效二進制編碼格式。 此外,pickle
模塊還擁有一個以 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)
參見
備注
- 1
不要把它與
marshal
模塊混淆。- 2
這就是為什么
lambda
函數(shù)不可以被封存:所有的匿名函數(shù)都有同一個名字:<lambda>
。- 3
拋出的異常有可能是
ImportError
或AttributeError
,也可能是其他異常。- 4
copy
模塊使用這一協(xié)議實現(xiàn)淺層 (shallow) 和深層 (deep) 復制操作。- 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.