資料串流格式
pickle
使用的資料格式是針對 Python 而設計的。好處是他不會受到外部標準(像是 JSON,無法紀錄指標共用)的限制;不過這也代表其他不是 Python 的程式可能無法重建 pickle 封裝的 Python 物件。
以預設設定來說,
pickle
使用相對緊湊的二進位形式來儲存資料。如果你需要盡可能地縮小檔案大小,你可以
壓縮
封裝的資料。
pickletools
含有工具可分析
pickle
所產生的資料流。
pickletools
的源始碼詳細地記載了所有 pickle 協定的操作碼(opcode)。
截至目前為止,共有六種不同版本的協定可用於封裝 pickle。數字越大版本代表你需要使用越新的 Python 版本來拆封相應的 pickle 封裝。
版本 0 的協定是最初「人類可讀」的版本,且可以向前支援早期版本的 Python。
版本 1 的協定使用舊的二進位格式,一樣能向前支援早期版本的 Python。
版本 2 的協定在 Python 2.3 中初次被引入。其可提供更高效率的
new-style classes
封裝過程。請參閱
PEP 307
以了解版本 2 帶來的改進。
版本 3 的協定在 Python 3.0 被新增。現在能支援封裝
bytes
的物件且無法被 2.x 版本的 Python 拆封。在 3.0~3.7 的 Python 預設使用 3 版協定。
版本 4 的協定在 Python 3.4 被新增。現在能支援超大物件的封裝、更多種型別的物件以及針對部份資料格式的儲存進行最佳化。從 Python 3.8 起,預設使用第 4 版協定。請參閱
PEP 3154
以了解第 4 版協定改進的細節。
版本 5 的協定在 Python 3.8 被新增。現在能支援帶外資料(Out-of-band data)並加速帶內資料的處理速度。請參閱
PEP 574
以了解第 5 版協定改進的細節。
資料序列化是一個比資料持久化更早出現的概念;雖然
pickle
可以讀寫檔案物件,但它並不處理命名持久物件的問題,也不處理對持久物件的並行存取、一個更棘手的問題。
pickle
模組可以將複雜物件轉換成位元組串流,也可以將位元組串流轉換回具有相同原始內部結構的物件。對這些位元組串流最常見的處理方式是將它們寫入檔案中,但也可以將它們透過網路傳送或儲存在一個資料庫中。
shelve
模組提供了一個簡單的介面來讓使用者在 DBM 風格的資料庫檔案中對物件進行封裝和拆封的操作。
模組介面
想要序列化一個物件,你只需要呼叫
dumps()
函式。而當你想要去序列化一個資料流時,你只需要呼叫
loads()
即可。不過,若你希望能各自對序列化和去序列化的過程中有更多的掌控度,你可以自訂一個
Pickler
或
Unpickler
物件。
pickle
模組提供以下常數:
pickle.
HIGHEST_PROTOCOL
一個整數,表示可使用的最高
協定版本
。這個值可作為
protocol
的數值傳給
dump()
和
dumps()
函式以及
Pickler
建構式。
pickle.
DEFAULT_PROTOCOL
一個整數,指示用於序列化的預設
協定版本
。有可能小於
HIGHEST_PROTOCOL
。目前的預設協定版本為 4,是在 Python 3.4 中首次引入的,且與先前版本不相容。
在 3.0 版的變更:
預設協定版本為 3。
在 3.8 版的變更:
預設協定版本為 4。
pickle
模組提供下列函式來簡化封裝的過程:
pickle.
dump
(
obj
,
file
,
protocol
=
None
,
*
,
fix_imports
=
True
,
buffer_callback
=
None
)
將被封裝成 pickle 形式的物件
obj
寫入到已開啟的
file object
file
。這等效於
Pickler(file,
protocol).dump(obj)
。
引數
file
、
protocol
、
fix_imports
和
buffer_callback
的意義與
Pickler
建構式中的相同。
在 3.8 版的變更:
新增
buffer_callback
引數。
pickle.
dumps
(
obj
,
protocol
=
None
,
*
,
fix_imports
=
True
,
buffer_callback
=
None
)
將被封裝為 pickle 形式的物件
obj
以
bytes
類別回傳,而非寫入進檔案。
引數
protocol
、
fix_imports
和
buffer_callback
的意義和
Pickler
建構式中的相同。
在 3.8 版的變更:
新增
buffer_callback
引數。
pickle.
load
(
file
,
*
,
fix_imports
=
True
,
encoding
=
'ASCII'
,
errors
=
'strict'
,
buffers
=
None
)
從已開啟的
檔案物件
file
中讀取已序列化的物件,並傳回其重建後的物件階層。這相當於呼叫
Unpickler(file).load()
。
模組會自動偵測 pickle 封包所使用的協定版本,所以無須另外指定。超出 pickle 封包表示範圍的位元組將被忽略。
引數
file
、
fix_imports
、
encoding
、
errors
、
strict
和
buffers
的意義和
Unpickler
建構式中的相同。
在 3.8 版的變更:
新增
buffer
引數。
pickle.
loads
(
data
,
/
,
*
,
fix_imports
=
True
,
encoding
=
'ASCII'
,
errors
=
'strict'
,
buffers
=
None
)
回傳從
data
的 pickle 封包重建後的物件階層。
data
必須是一個
bytes-like object
。
模組會自動偵測 pickle 封包所使用的協定版本,所以無須另外指定。超出 pickle 封包表示範圍的位元組將被忽略。
引數
fix_imports
、
encoding
、
errors
、
strict
和
buffers
的意義與
Unpickler
建構式所用的相同。
在 3.8 版的變更:
新增
buffer
引數。
pickle
模組定義了以下三種例外:
exception
pickle.
PickleError
繼承
Exception
類別。一個在封裝或拆封時遭遇其他例外時通用的基底類別。
exception
pickle.
UnpicklingError
拆封物件時遇到問題(如資料毀損或違反安全性原則等)所引發的意外。繼承自
PickleError
類別。
拆封的時候還是可能會遭遇其他不在此列的例外(例如:AttributeError、EOFError、ImportError、或 IndexError),請注意。
引入模組
pickle
時會帶來三個類別:
Pickler
、
Unpickler
和
PickleBuffer
:
class
pickle.
Pickler
(
file
,
protocol
=
None
,
*
,
fix_imports
=
True
,
buffer_callback
=
None
)
接受一個用以寫入 pickle 資料流的二進位檔案。
可選引數
protocol
接受整數,用來要求封裝器(pickler)使用指定的協定;支援從 0 版起到
HIGHEST_PROTOCOL
版的協定。如未指定,則預設為
DEFAULT_PROTOCOL
。若指定了負數,則視為選擇
HIGHEST_PROTOCOL
。
引數
file
必須支援可寫入單一位元組引數的 write() 方法。只要滿足此條件,傳入的物件可以是一個硬碟上二進位檔案、一個
io.BytesIO
實例或任何其他滿足這個介面要求的物件。
若
fix_imports
設為 true 且
protocol
版本小於 3,本模組會嘗試將 Python 3 的新模組名稱轉換為 Python 2 所支援的舊名,以讓 Python 2 能正確地讀取此資料流。
如果
buffer_callback
是
None
(預設值),緩衝區的視圖會作為 pickle 封裝串流的一部分被序列化進
file
中。
如果
buffer_callback
不是
None
,則它可以被多次呼叫並回傳一個緩衝區的視圖。如果回呼函式回傳一個假值(例如
None
),則所給的緩衝區將被視為
帶外資料
;否則,該緩衝區將被視為 pickle 串流的帶內資料被序列化。
如果
buffer_callback
不是
None
且
protocol
是
None
或小於 5 則會報錯。
在 3.8 版的變更:
新增
buffer_callback
引數。
dump
(
obj
)
將已封裝(pickled)的
obj
寫入已在建構式中開啟的對應檔案。
persistent_id
(
obj
)
預設不進行任何動作。這是一種抽象方法,用於讓後續繼承這個類別的物件可以覆寫本方法函式。
如果
persistent_id()
回傳
None
,則
obj
會照一般的方式進行封裝(pickling)。若回傳其他值,則
Pickler
會將該值作為
obj
的永久識別碼回傳。此永久識別碼的意義應由
Unpickler.persistent_load()
定義。請注意
persistent_id()
回傳的值本身不能擁有自己的永久識別碼。
關於細節與用法範例請見
外部物件持久化
。
在 3.13 版的變更:
在 C 的
Pickler
實作中的增加了這個方法的預設實作。
dispatch_table
封裝器(pickler)物件含有的的調度表是一個
縮減函式
(reduction function)的註冊表,可以使用
copyreg.pickle()
來宣告這類縮減函式。它是一個以類別為鍵、還原函式為值的映射表。縮減函式應準備接收一個對應類別的引數,並應遵循與
__reduce__()
方法相同的介面。
預設情況下,封裝器(pickler)物件不會有
dispatch_table
屬性,而是會使用由
copyreg
模組管理的全域調度表。不過,若要自訂某個封裝器(pickler)物件的序列化行為,可以將
dispatch_table
屬性設置為類字典物件。另外,如果
Pickler
的子類別具有
dispatch_table
屬性,那麼這個屬性將作為該子類別實例的預設調度表。
關於用法範例請見
調度表
。
在 3.3 版被加入.
reducer_override
(
obj
)
一個可以在
Pickler
子類別中被定義的縮減器(reducer)。這個方法的優先度高於任何其他
分派表
中的縮減器。他應該要有和
__reduce__()
方法相同的函式介面,且可以可選地回傳
NotImplemented
以後備(fallback)使用
分派表
中登錄的縮減方法來封裝
obj
。
請查閱
針對型別、函式或特定物件定製縮減函式
來參考其他較詳細的範例。
在 3.8 版被加入.
fast
已棄用。如果設置為 true,將啟用快速模式。快速模式會停用備忘(memo),因此能透過不產生多餘的 PUT 操作碼(OpCode)來加速封裝過程。它不應被用於自我參照物件,否則將導致
Pickler
陷入無限遞迴。
使用
pickletools.optimize()
以獲得更緊湊的 pickle 輸出。
class
pickle.
Unpickler
(
file
,
*
,
fix_imports
=
True
,
encoding
=
'ASCII'
,
errors
=
'strict'
,
buffers
=
None
)
這個物件接受一個二進位檔案
file
來從中讀取 pickle 資料流。
協定版本號會被自動偵測,所以不需要在這邊手動輸入。
參數
file
必須擁有三個方法,分別是接受整數作為引數的 read() 方法、接受緩衝區作為引數的 readinto() 方法以及不需要引數的 readline() 方法,如同在
io.BufferedIOBase
的介面一樣。因此,
file
可以是一個以二進位讀取模式開啟的檔案、一個
io.BytesIO
物件、或任何符合此介面的自訂物件。
可選引數
fix_imports
、
encoding
和
errors
用來控制 Python 2 pickle 資料的相容性支援。如果
fix_imports
為 true,則 pickle 模組會嘗試將舊的 Python 2 模組名稱映射到 Python 3 中使用的新名稱。
encoding
和
errors
告訴 pickle 模組如何解碼由 Python 2 pickle 封裝的 8 位元字串實例;
encoding
和
errors
預設分別為 'ASCII' 和 'strict'。
encoding
可以設定為 'bytes' 以將這些 8 位元字串實例讀為位元組物件。而由 Python 2 封裝的 NumPy 陣列、
datetime
、
date
和
time
的實例則必須使用
encoding='latin1'
來拆封。
如果
buffers
是
None
(預設值),那麼去序列化所需的所有資料都必須已經包含在 pickle 串流中。這意味著當初在建立對應的
Pickler
時(或在呼叫
dump()
或
dumps()
時)*buffer_callback* 引數必須為
None
。
如果
buffers
不是
None
,則其應該是一個可疊代物件,內含數個支援緩衝區的物件,並且每當 pickle 串流引用一個
帶外
緩衝區視圖時將會被照順序消耗。這些緩衝資料當初建立時應已按照順序給定於 Pickler 物件中的
buffer_callback
。
在 3.8 版的變更:
新增
buffer
引數。
load
(
)
開啟先前被傳入建構子的檔案,從中讀取一個被 pickle 封裝的物件,並回傳重建完成的物件階層。超過 pickle 表示範圍的位元組會被忽略。
persistent_load
(
pid
)
預設會引發
UnpicklingError
例外。
若有定義,則
persistent_load()
將回傳符合持久化識別碼
pid
的物件。如果遭遇了無效的持久化識別碼,則會引發
UnpicklingError
。
關於細節與用法範例請見
外部物件持久化
。
在 3.13 版的變更:
在 C 的
Unpickler
實作中的增加了這個方法的預設實作。
find_class
(
module
,
name
)
如有需要將引入
module
,並從中回傳名為
name
的物件,這裡的
module
和
name
引數接受的輸入是
str
物件。注意,雖然名稱上看起來不像,但
find_class()
亦可被用於尋找其他函式。
子類別可以覆寫此方法以控制可以載入哪些類型的物件、以及如何載入它們,從而潛在地降低安全性風險。詳情請參考
限制全域物件
。
引發一個附帶引數
module
、
name
的
稽核事件
pickle.find_class
。
class
pickle.
PickleBuffer
(
buffer
)
一個表示了含有可封裝資料緩衝區的包裝函式(wrapper function)。
buffer
必須是一個
提供緩衝區
的物件,例如一個
類位元組物件
或 N 維陣列。
PickleBuffer
本身就是一個提供緩衝區的物件,所以是能夠將其提供給其它「預期收到含有緩衝物件的 API」的,比如
memoryview
。
PickleBuffer
物件僅能由 5 版或以上的 pickle 協定進行封裝。該物件亦能被作為帶外資料來進行
帶外資料序列化
在 3.8 版被加入.
raw
(
)
回傳此緩衝區底層記憶體區域的
memoryview
。被回傳的物件是一個(在 C 語言的 formatter 格式中)以
B
(unsigned bytes) 二進位格式儲存、一維且列連續(C-contiguous)的 memoryview。如果緩衝區既不是列連續(C-contiguous)也不是行連續(Fortran-contiguous)的,則會引發
BufferError
。
字串、位元組物件、位元組陣列;
元組(tuple)、串列(list)、集合(set)和僅含有可封裝物件的字典;
在模組最表面的層級就能被存取的函式(內建或自訂的皆可,不過僅限使用
def
定義的函式,
lambda
函式不適用);
在模組最表面的層級就能被存取的類別;
實例,只要在呼叫了
__getstate__()
後其回傳值全都是可封裝物件。(詳情請參閱
Pickling 類別實例
)。
嘗試封裝無法封裝的物件會引發
PicklingError
例外;注意當這種情況發生時,可能已經有未知數量的位元組已被寫入到檔案。嘗試封裝深度遞迴的資料結構可能會導致其超出最大遞迴深度,在這種情況下會引發
RecursionError
例外。你可以(小心地)使用
sys.setrecursionlimit()
來提高此上限。
請注意,函式(內建及自訂兩者皆是)是依據完整的
限定名稱
來封裝,而非依其值。
這意味著封裝時只有函式名稱、所屬的模組和所屬的類別名稱會被封裝。函式本身的程式碼及其附帶的任何屬性均不會被封裝。因此,在拆封該物件的環境中,定義此函式的模組必須可被引入,且該模組必須包含具此命名之物件,否則將引發例外。
同樣情況,類別是依照其完整限定名稱來進行封裝,因此在進行拆封的環境中會具有同上的限制。類別中的程式碼或資料皆不會被封裝,因此在以下範例中,注意到類別屬性
attr
在拆封的環境中不會被還原:
class Foo:
attr = 'A class attribute'
picklestring = pickle.dumps(Foo)
這些限制就是可封裝的函式和類別必須被定義在模組頂層的原因。
同樣地,當類別實例被封裝時,它所屬類別具有的程式碼和資料不會被一起封裝。只有實例資料本身會被封裝。這是有意而為的,因為如此你才可以在類別中修正錯誤或新增其他方法,且於此同時仍能夠載入使用較早期版本的類別所建立的物件實例。如果你預計將有長期存在的物件、且該物件將經歷許多版本的更替,你可以在物件中存放一個版本號,以便未來能透過 __setstate__()
方法來進行適當的版本轉換。
Pickling 類別實例
在這一個章節,我們會講述如何封裝或拆封一個物件實例的相關機制,以方便你進行自訂。
大部分的實例不需要額外的程式碼就已經是可封裝的了。在這樣的預設狀況中,pickle 模組透過自省機制來取得類別及其實例的屬性。當類別實例被拆封時,其 __init__()
方法通常不會被呼叫。預設行為首先會建立一個未初始化的實例,然後還原紀錄中的屬性。以下程式碼的實作展示了前述行為:
def save(obj):
return (obj.__class__, obj.__dict__)
def restore(cls, attributes):
obj = cls.__new__(cls)
obj.__dict__.update(attributes)
return obj
被封裝的目標類別可以提供一個或數個下列特殊方法來改變 pickle 的預設行為:
object.__getnewargs_ex__()
在第 2 版協定或更新的版本中,有實作 __getnewargs_ex__()
方法的類別,可以決定在拆封時要傳遞給 __new__()
方法的值。該方法必須回傳一個 (args, kwargs)
的組合,其中 args 是一個位置引數的元組(tuple),kwargs 是一個用於建構物件的命名引數字典。這些資訊將在拆封時傳遞給 __new__()
方法。
如果目標類別的方法 __new__()
需要僅限關鍵字的參數時,你應該實作此方法。否則,為了提高相容性,建議你改為實作 __getnewargs__()
。
在 3.6 版的變更: 在第 2、3 版的協定中現在改為使用 __getnewargs_ex__()
。
object.__getnewargs__()
此方法與 __getnewargs_ex__()
的目的一樣,但僅支援位置參數。它必須回傳一個由傳入引數所組成的元組(tuple)args
,這些引數會在拆封時傳遞給 __new__()
方法。
當有定義 __getnewargs_ex__()
的時候便不會呼叫 __getnewargs__()
。
在 3.6 版的變更: 在 Python 3.6 之前、版本 2 和版本 3 的協定中,會呼叫 __getnewargs__()
而非 __getnewargs_ex__()
。
object.__getstate__()
目標類別可以透過覆寫方法 __getstate__()
進一步影響其實例被封裝的方式。封裝時,呼叫該方法所回傳的物件將作為該實例的內容被封裝、而非一個預設狀態。以下列出幾種預設狀態:
沒有 __dict__
和 __slots__
實例的類別,其預設狀態為 None
。
有 __dict__
實例、但沒有 __slots__
實例的類別,其預設狀態為 self.__dict__
。
有 __dict__
和 __slots__
實例的類別,其預設狀態是一個含有兩個字典的元組(tuple),該二字典分別為 self.__dict__
本身,和紀錄欄位(slot)名稱和值對應關係的字典(只有含有值的欄位(slot)會被紀錄其中)。
沒有 __dict__
但有 __slots__
實例的類別,其預設狀態是一個二元組(tuple),元組中的第一個值是 None
,第二個值則是紀錄欄位(slot)名稱和值對應關係的字典(與前一項提到的字典是同一個)。
在 3.11 版的變更: 在 object
類別中增加預設的 __getstate__()
實作。
object.__setstate__(state)
在拆封時,如果類別定義了 __setstate__()
,則會使用拆封後的狀態呼叫它。在這種情況下,紀錄狀態的物件不需要是字典(dictionary)。否則,封裝時的狀態紀錄必須是一個字典,其紀錄的項目將被賦值給新實例的字典。
如果 __reduce__()
在封裝時回傳了 None
狀態,則拆封時就不會去呼叫 __setstate__()
。
參閱 處裡紀錄大量狀態的物件 以了解 __getstate__()
和 __setstate__()
的使用方法。
在拆封時,某些方法如 __getattr__()
、__getattribute__()
或 __setattr__()
可能會在建立實例時被呼叫。如果這些方法依賴了某些實例內部的不變性,則應實作 __new__()
以建立此不變性,因為在拆封實例時不會呼叫 __init__()
。
如稍後所演示,pickle 並不直接使用上述方法。這些方法實際上是實作了 __reduce__()
特殊方法的拷貝協定(copy protocol)。拷貝協定提供了統一的介面,以檢索進行封裝及複製物件時所需的資料。
直接在類別中實作 __reduce__()
雖然功能強大但卻容易導致出錯。因此,設計類別者應盡可能使用高階介面(例如,__getnewargs_ex__()
、__getstate__()
和 __setstate__()
)。不過,我們也將展示一些特例狀況,在這些狀況中,使用 __reduce__()
可能是唯一的選擇、是更有效率的封裝方法或二者兼備。
object.__reduce__()
目前的介面定義如下。 __reduce__()
方法不接受引數,且應回傳一個字串或一個元組(元組一般而言是較佳的選擇;所回傳的物件通常稱為「縮減值」)。
如果回傳的是字串,該字串應被解讀為一個全域變數的名稱。它應是該物件相對其所在模組的本地名稱;pickle 模組會在模組命名空間中尋找,以確定該物件所在的模組。這種行為通常對於單例物件特別有用。
當回傳一個元組時,其長度必須介於兩至六項元素之間。可選項可以被省略,或者其值可以被設為 None
。各項物件的語意依序為:
一個將會被呼叫來建立初始版本物件的可呼叫物件。
一個用於傳遞引數給前述物件的元組。如果前述物件不接受引數輸入,則你仍應在這裡給定一個空元組。
可選項。物件狀態。如前所述,會被傳遞給該物件的 __setstate__()
方法。如果該物件沒有實作此方法,則本值必須是一個字典,且其將會被新增到物件的 __dict__
屬性中。
可選項。一個用來提供連續項目的疊代器(而非序列)。這些項目將個別透過 obj.append(item)
方法或成批次地透過 obj.extend(list_of_items)
方法被附加到物件中。主要用於串列(list)子類別,但只要其他類別具有相應的 append 和 extend 方法以及相同的函式簽章(signature)就也可以使用。 (是否會呼叫 append()
或 extend()
方法將取決於所選用的 pickle 協定版本以及要附加的項目數量,因此必須同時支援這兩種方法。)
可選項。一個產生連續鍵值對的疊代器(不是序列)。這些項目將以 obj[key] = value
方式被儲存到物件中。主要用於字典(dictionary)子類別,但只要有實現了 __setitem__()
的其他類別也可以使用。
可選項。一個具有 (obj, state)
函式簽章(signature)的可呼叫物件。該物件允許使用者以可編寫的邏輯,而不是物件 obj
預設的 __setstate__()
靜態方法去控制特定物件的狀態更新方式。如果這個物件不是 None
,這個物件的呼叫優先權將優於物件 obj
的 __setstate__()
。
在 3.8 版被加入: 加入第六個可選項(一個 (obj, state)
元組)。
外部物件持久化
為了方便物件持久化,pickle
模組支援對被封裝資料串流以外的物件參照。被參照的物件是透過一個持久化 ID 來參照的,這個 ID 應該要是字母數字字元(alphanumeric)組成的字串(協定 0) 或者是任意的物件(任何較新的協定)。
pickle
沒有定義要如何解決或分派這個持久化 ID 的問題;故其處理方式有賴使用者自行定義在封裝器(pickler)以及拆封器(unpickler)中。方法的名稱各自為 persistent_id()
和 persistent_load()
。
要封裝具有外部持久化 ID 的物件,封裝器(pickler)必須擁有一個自訂的方法 persistent_id()
,這個方法將接收一個物件作為參數,並回傳 None
或該物件的持久化 ID。當回傳 None
時,封裝器會正常地封裝該物件。當回傳一個持久化 ID 字串時,封裝器會封裝該物件並加上一個標記,讓拆封器(unpickler)能識別它是一個持久化 ID。
要拆封外部物件,拆封器(unpickler)必須有一個自訂的 persistent_load()
方法,該方法應接受一個持久化 ID 物件,並回傳相對應的物件。
以下是一個完整的範例,用以說明如何使用持久化 ID 來封裝具外部參照的物件。
# 展示如何使用持久化 ID 來封裝外部物件的簡單範例
import pickle
import sqlite3
from collections import namedtuple
# 代表資料庫中紀錄的一個簡易類別
MemoRecord = namedtuple("MemoRecord", "key, task")
class DBPickler(pickle.Pickler):
def persistent_id(self, obj):
# 我們派發出一個持久 ID,而不是像一般類別實例那樣封裝 MemoRecord。
if isinstance(obj, MemoRecord):
# 我們的持久 ID 就是一個元組,裡面包含一個標籤和一個鍵,指向資料庫中的特定紀錄。
return ("MemoRecord", obj.key)
else:
# 如果 obj 沒有持久 ID,則回傳 None。這表示 obj 像平常那樣封裝即可。
return None
class DBUnpickler(pickle.Unpickler):
def __init__(self, file, connection):
super().__init__(file)
self.connection = connection
def persistent_load(self, pid):
# 每當遇到持久 ID 時,此方法都會被呼叫。
# pid 是 DBPickler 所回傳的元組。
cursor = self.connection.cursor()
type_tag, key_id = pid
if type_tag == "MemoRecord":
# 從資料庫中抓取所引用的紀錄並回傳。
cursor.execute("SELECT * FROM memos WHERE key=?", (str(key_id),))
key, task = cursor.fetchone()
return MemoRecord(key, task)
else:
# 如果無法回傳正確的物件,則必須引發錯誤。
# 否則 unpickler 會誤認為 None 是持久 ID 所引用的物件。
raise pickle.UnpicklingError("unsupported persistent object")
def main():
import io
import pprint
# 初始化資料庫。
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,))
# 抓取要封裝的紀錄。
cursor.execute("SELECT * FROM memos"
)
memos = [MemoRecord(key, task) for key, task in cursor]
# 使用我們自訂的 DBPickler 來保存紀錄。
file = io.BytesIO()
DBPickler(file).dump(memos)
print("被封裝的紀錄:")
pprint.pprint(memos)
# 更新一筆紀錄(測試用)。
cursor.execute("UPDATE memos SET task='learn italian' WHERE key=1")
# 從 pickle 資料流中載入紀錄。
file.seek(0)
memos = DBUnpickler(file, conn).load()
print("已拆封的紀錄:")
pprint.pprint(memos)
if __name__ == '__main__':
main()
調度表
如果你希望在不干擾其他物件正常封裝的前提下建立一個針對特定物件的封裝器,你可建立一個有私密調度表的封裝器。
由 copyreg
模組管理的全域調度表可以 copyreg.dispatch_table
呼叫。你可以透過這個方式來基於原始 copyreg.dispatch_table
建立一個修改過的版本,作為你的專屬用途的調度表。
舉例來說:
f = io.BytesIO()
p = pickle.Pickler(f)
p.dispatch_table = copyreg.dispatch_table.copy()
p.dispatch_table[SomeClass] = reduce_SomeClass
建立了一個 pickle.Pickler
,其中含有專門處裡 SomeClass
類別的專屬調度表。此外,你也可以寫作::
class MyPickler(pickle.Pickler):
dispatch_table = copyreg.dispatch_table.copy()
dispatch_table[SomeClass] = reduce_SomeClass
f = io.BytesIO()
p = MyPickler(f)
這樣可產生相似的結果,唯一不同的是往後所有 MyPickler
預設都會使用這個專屬調度表。最後,如果將程式寫為::
copyreg.pickle(SomeClass, reduce_SomeClass)
f = io.BytesIO()
p = pickle.Pickler(f)
則會改變 copyreg
模組內建、所有使用者共通的調度表。
處裡紀錄大量狀態的物件
以下的範例展示了如何修改針對特定類別封裝時的行為。下面的 TextReader
類別會開啟一個文字檔案,並在每次呼叫其 readline()
方法時返回目前行編號與該行內容。如果 TextReader
實例被封裝,所有除了檔案物件之外的屬性成員都會被保存。在該實例被拆封時,檔案將被重新開啟,並從上次的位置繼續讀取。這個行為的達成是透過 __setstate__()
和 __getstate__()
方法來實作的。:
class TextReader:
"""列出文字檔案中的行並對其進行編號。"""
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):
# 從 self.__dict__ 中複製物件的狀態。包含了所有的實例屬性。
# 使用 dict.copy() 方法以避免修改原始狀態。
state = self.__dict__.copy()
# 移除不可封裝的項目。
del state['file']
return state
def __setstate__(self, state):
# 恢復實例屬性(即 filename 和 lineno)。
self.__dict__.update(state)
# 恢復到先前開啟了檔案的狀態。為此,我們需要重新開啟它並一直讀取到行數編號相同。
file = open(self.filename)
for _ in range(self.lineno):
file.readline()
# 存檔。
self.file = file
可以這樣實際使用::
>>> reader = TextReader("hello.txt")
>>> reader.readline()
'1: Hello world!'
>>> reader.readline()
'2: I am line number two.'
>>> new_reader = pickle.loads(pickle.dumps(reader))
>>> new_reader.readline()
'3: Goodbye!'
在 3.8 版被加入.
有時候,dispatch_table
的彈性空間可能不夠。尤其當我們想要使用型別以外的方式來判斷如何使用自訂封裝、或者我們想要自訂特定函式和類別的封裝方法時。
如果是這樣的話,可以繼承 Pickler
類別並實作一個 reducer_override()
方法。此方法可以回傳任意的縮減元組(參閱 __reduce__()
)、也可以回傳 NotImplemented
以使用後備的原始行為方案。
如果 dispatch_table
和 reducer_override()
都被定義了的話,reducer_override()
的優先度較高。
出於效能考量,處裡以下物件可能不會呼叫 reducer_override()
:None
、True
、False
,以及 int
、float
、bytes
、str
、dict
、set
、frozenset
、list
和 tuple
的實例。
以下是一個簡單的例子,我們示範如何允許封裝和重建給定的類別::
import io
import pickle
class MyClass:
my_attribute = 1
class MyPickler(pickle.Pickler):
def reducer_override(self, obj):
"""MyClass 的自訂縮減函式。"""
if getattr(obj, "__name__", None) == "MyClass":
return type,
(obj.__name__, obj.__bases__,
{'my_attribute': obj.my_attribute})
else:
# 遭遇其他物件,則使用一般的縮減方式
return NotImplemented
f = io.BytesIO()
p = MyPickler(f)
p.dump(MyClass)
del MyClass
unpickled_class = pickle.loads(f.getvalue())
assert isinstance(unpickled_class, type)
assert unpickled_class.__name__ == "MyClass"
assert unpickled_class.my_attribute == 1
在 3.8 版被加入.
pickle
模組會被用於用於傳輸龐大的資料。此時,將複製記憶體的次數降到最低以保持效能變得很重要。然而,pickle
模組的正常操作過程中,當它將物件的圖狀結構(graph-like structure)轉換為連續的位元組串流時,本質上就涉及將資料複製到封裝流以及從封裝流複製資料。
如果供給者(被傳遞物件的型別的實作)與消費者(資訊交換系統的實作)都支援由 pickle 協定 5 或更高版本提供的帶外傳輸功能,則可以避免此一先天限制。
供給者 API
要封裝的大型資料物件,則必須實作一個針對 5 版協定及以上的 __reduce_ex__()
方法,該方法應回傳一個 PickleBuffer
實例來處理任何大型資料(而非回傳如 bytes
物件)。
一個 PickleBuffer
物件指示了當下底層的緩衝區狀態適合進行帶外資料傳輸。這些物件仍然相容 pickle
模組的一般使用方式。消費者程式也可以選擇介入,指示 pickle
他們將自行處理這些緩衝區。
消費者 API
一個資訊交換系統可以決定要自行處裡序列化物件圖時產生的 PickleBuffer
物件。
傳送端需要傳遞一個呼叫緩衝區的回呼函式給 Pickler
(或 dump()
或 dumps()
函式)的 buffer_callback 引數,使每次生成 PickleBuffer
時,該物件在處理物件圖時能被呼叫。除了一個簡易標記以外,由 buffer_callback 累積的緩衝區資料不會被複製到 pickle 串流中。
接收端需要傳遞一個緩衝區物件給 Unpickler
(或 load()
或 loads()
函式)的 buffers 引數。該物件須是一個可疊代的(iterable)緩衝區(buffer)物件,其中包含傳遞給 buffer_callback 的緩衝區物件。這個可疊代物件的緩衝區順序應該與它們當初被封裝時傳遞給 buffer_callback 的順序相同。這些緩衝區將提供物件重建所需的資料,以使重建器能還原出那個當時產生了 PickleBuffer
的物件。
在傳送與接收端之間,通訊系統可以自由實作轉移帶外緩衝區資料的機制。該機制可能可以利用共用記憶體機制或根據資料類型特定的壓縮方式來最佳化執行速度。
這一個簡單的範例展示了如何實作一個可以參與帶外緩衝區封裝的 bytearray
子類別::
class ZeroCopyByteArray(bytearray):
def __reduce_ex__(self, protocol):
if protocol >= 5:
return type(self)._reconstruct, (PickleBuffer(self),), None
else:
# PickleBuffer 在 pickle 協定 <= 4 時禁止使用。
return type(self)._reconstruct, (bytearray(self),)
@classmethod
def _reconstruct(cls, obj):
with memoryview(obj) as m:
# 取得對原始緩衝區物件的控制
obj = m.obj
if type(obj) is cls:
# 若原本的緩衝區物件是 ZeroCopyByteArray,則直接回傳。
return obj
else:
return cls(obj)
如果型別正確,重建器(_reconstruct
類別方法)會回傳當時提供緩衝區的物件。這個簡易實作可以模擬一個無複製行為的重建器。
在使用端,我們可以用一般的方式封裝這些物件,當我們拆封時會得到一個原始物件的副本::
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: 曾進行過複製運算
但如果我們傳一個 buffer_callback 並在去序列化時正確回傳積累的緩衝資料,我們就能拿回原始的物件::
b = ZeroCopyByteArray(b"abc")
buffers = []
data = pickle.dumps(b, protocol=5, buffer_callback=buffers.append)
new_b = pickle.loads(data, buffers=buffers)
print(b == new_b) # True
print(b is new_b) # True: 沒有進行過複製
此範例是因為受限於 bytearray
會自行分配記憶體:你無法建立以其他物件的記憶體為基礎的 bytearray
實例。不過第三方資料型態(如 NumPy 陣列)則可能沒有這個限制,而允許在不同程序或系統之間傳輸資料時使用零拷貝封裝(或儘可能地減少拷貝次數)。
PEP 574 -- 第 5 版 Pickle 協定的帶外資料(out-of-band data)處裡
限制全域物件
預設情況下,拆封過程將會引入任何在 pickle 資料中找到的類別或函式。對於許多應用程式來說,這種行為是不可接受的,因為它讓拆封器能夠引入並執行任意程式碼。請參見以下 pickle 資料流在載入時的行為::
>>> import pickle
>>> pickle.loads(b"cos\nsystem\n(S'echo hello world'\ntR.")
hello world
在這個例子中,拆封器會引入 os.system()
函式,然後執行命令「echo hello world」。雖然這個例子是無害的,但不難想像可以這個方式輕易執行任意可能對系統造成損害的命令。
基於以上原因,你可能會希望透過自訂 Unpickler.find_class()
來控制哪些是能夠被拆封的內容。與其名稱字面意義暗示的不同,實際上每當你請求一個全域物件(例如,類別或函式)時,就會呼叫 Unpickler.find_class()
。因此,可以透過這個方法完全禁止全域物件或將其限制在安全的子集合。
以下是一個僅允許從 builtins
模組中載入少數安全類別的拆封器(unpickler)的例子::
import builtins
import io
import pickle
safe_builtins = {
'range',
'complex',
'set',
'frozenset',
'slice',
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
# 只允許幾個內建的安全類別
if module == "builtins" and name in safe_builtins:
return getattr(builtins, name)
# 完全禁止任何其他類別
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
(module, name))
def restricted_loads(s):
"""一個模擬 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
中的 marshalling API 或其他第三方解決方案。
較近期的 pickle 協定版本(從 2 版協定開始)為多種常見功能和內建型別提供了高效率的二進位編碼。此外,pickle
模組還具備一個透明化的、以 C 語言編寫的最佳化工具。
最簡單的使用方式,呼叫 dump()
和 load()
函式。:
import pickle
# 任意 pickle 支援的物件。
data = {
'a': [1, 2.0, 3+4j],
'b': ("string", b"byte string"),
'c': {None, True, False}
with open('data.pickle', 'wb') as f:
# 使用可用的最高協定來封裝 'data' 字典。
pickle.dump(data, f, pickle.HIGHEST_PROTOCOL)
以下範例可以讀取前述程式所封裝的 pickle 資料。:
import pickle
with open('data.pickle', 'rb') as f:
# 會自動檢測資料使用的協定版本,因此我們不需要手動指定。
data = pickle.load(f)
copyreg
模組註冊擴充型別的 Pickle 介面建構子。
pickletools
模組用於分析或處裡被封裝資料的工具。
shelve
模組索引式資料庫;使用 pickle
實作。
copy
模組物件的淺層或深度拷貝。
marshal
模組內建型別的高效能序列化。
[1]
不要將此模組與 marshal
模組混淆
[2]
這就是為什麼 lambda
函式無法被封裝:所有 lambda
函式共享相同的名稱:<lambda>
。
[3]
引發的例外應該是 ImportError
或 AttributeError
,但也可能是其他例外。
[4]
copy
模組使用此協定進行淺層及深層複製操作。
[5]
協定 0 中限制僅能使用英文字母或數字字元來分配持久化 ID 是因為持久化 ID 是由換行符號所分隔的。因此,如果持久化 ID 中出現任何形式的換行字元,將導致封裝資料變得無法讀取。
2001-2025, Python Software Foundation.
This page is licensed under the Python Software Foundation License Version 2.
Examples, recipes, and other code in the documentation are additionally licensed under the Zero Clause BSD License.
See History and License for more information.