PEP 307 – 擴充套件 pickle 協議
- 作者:
- Guido van Rossum, Tim Peters
- 狀態:
- 最終版
- 型別:
- 標準跟蹤
- 建立日期:
- 2003 年 1 月 31 日
- Python 版本:
- 2.3
- 釋出歷史:
- 2003 年 2 月 7 日
引言
在 Python 2.2 中 pickling 新式物件的方式有些笨拙,與舊式類例項相比,會導致 pickle 大小膨脹。本 PEP 詳細介紹了一個新的 pickle 協議,該協議將在 Python 2.3 中引入,可以解決這個問題以及許多其他 pickle 問題。
指定新的 pickle 協議有兩個方面:需要指定構成 pickled 資料的位元組流,以及需要指定物件與 pickling 和 unpickling 引擎之間的介面。本 PEP 側重於 API 問題,儘管有時也會提及位元組流格式細節來解釋某種選擇。pickle 位元組流格式由標準庫模組 pickletools.py 正式記錄(該模組已在 Python 2.3 的 CVS 中提交)。
本 PEP 試圖完整地記錄 pickled 物件與 pickling 過程之間的介面,透過指定“本 PEP 新增”來突出新內容。(除了用於指定 pickler 的 pickling 協議的 API 更改外,不完全涵蓋用於呼叫 pickling 或 unpickling 的介面。)
動機
Pickling 新式物件會導致 pickle 大小嚴重膨脹。例如
class C(object): # Omit "(object)" for classic class
pass
x = C()
x.foo = 42
print len(pickle.dumps(x, 1))
舊式物件的二進位制 pickle 佔用了 33 位元組,新式物件則佔用了 86 位元組。
膨脹的原因很複雜,但主要歸因於新式物件需要 __reduce__ 才能進行 pickling。經過充分考慮,我們得出結論,減少新式物件 pickle 大小的唯一方法是向 pickle 協議新增新的操作碼。最終結果是,使用新協議,上述示例中的 pickle 大小為 35(開頭使用了兩個額外的位元組來指示協議版本,儘管這並非嚴格必需)。
協議版本
以前,pickling(但不是 unpickling)區分文字模式和二進位制模式。根據設計,二進位制模式是文字模式的超集,unpickler 不需要提前知道傳入的 pickle 是使用文字模式還是二進位制模式。用於 unpickling 的虛擬機器在模式上是相同的;某些操作碼在文字模式下根本不使用。
追溯性地,文字模式現在稱為協議 0,二進位制模式稱為協議 1。新協議稱為協議 2。遵循 pickling 協議的傳統,協議 2 是協議 1 的超集。但為了避免未來的 pickling 協議需要成為最舊協議的超集,協議 2 pickle 的開頭插入了一個新的操作碼,指示它使用的是協議 2。到目前為止,Python 的每個版本都能夠讀取所有先前版本編寫的 pickle。當然,在協議 N 下編寫的 pickle 無法被引入協議 N 之前的 Python 版本讀取。
用於 pickling 的幾個函式、方法和建構函式以前接受一個名為‘bin’的位置引數,這是一個標誌,預設為 0,表示二進位制模式。此引數已重新命名為‘protocol’,現在給出協議編號,仍預設為 0。
碰巧,在以前的 Python 版本中,將 2 作為‘bin’引數具有與將 1 作為引數相同的效果。儘管如此,這裡添加了一個特殊情況:傳遞負數會選擇特定實現支援的最高協議版本。這在以前的 Python 版本中也有效,因此可以用來選擇最高可用協議,且兼顧向後和向前相容。此外,pickle 和 cPickle 模組都提供了一個新的模組常量 HIGHEST_PROTOCOL,它等於模組可以讀取的最高協議編號。這比傳遞 -1 更清晰,但在 Python 2.3 之前無法使用。
pickle.py 模組一直支援將‘bin’值作為關鍵字引數而不是位置引數傳遞。(不推薦這樣做,因為 cPickle 只接受位置引數,但它有效……)將‘bin’作為關鍵字引數傳遞已被棄用,在這種情況下會發出 PendingDeprecationWarning 警告。您必須使用 -Wa 或其變體來呼叫 Python 直譯器才能看到 PendingDeprecationWarning 訊息。在 Python 2.4 中,警告類可能會升級為 DeprecationWarning。
安全問題
在以前的 Python 版本中,unpickling 會對某些操作進行“安全檢查”,拒絕呼叫未透過“可安全 unpickling”標記的函式或建構函式,這些標記是透過具有 __safe_for_unpickling__ 屬性設定為 1,或在全域性登錄檔 copy_reg.safe_constructors 中註冊來實現的。
此功能給人一種虛假的安全感:沒有人進行過必要的、廣泛的程式碼審計來證明 unpickling 不受信任的 pickle 不會呼叫不希望的程式碼,事實上 Python 2.2 pickle.py 模組中的錯誤使得繞過這些安全措施變得容易。
我們堅信,在網際網路上,知道自己正在使用一個不安全的協議比信任一個未經充分檢查其實現而聲稱安全的協議要好。即使是廣泛使用的協議的高質量實現,也經常會發現存在缺陷;Python 的 pickle 實現如果沒有更多的時間投入,根本無法做出這樣的保證。因此,從 Python 2.3 開始,所有 unpickling 上的安全檢查都被正式移除,並替換為此警告:
Warning
請勿 unpickle 來自不可信或未經驗證來源的資料。
儘管存在安全檢查,但同樣的警告也適用於以前的 Python 版本。
擴充套件的 __reduce__ API
類可以利用幾個 API 來控制 pickling。其中最受歡迎的可能是 __getstate__ 和 __setstate__;但最強大的是 __reduce__。(還有一個 __getinitargs__,我們將在下面新增 __getnewargs__。)
有幾種方法可以提供 __reduce__ 功能:類可以實現 __reduce__ 方法或 __reduce_ex__ 方法(見下一節),或者可以在 copy_reg 中宣告一個 reduce 函式(copy_reg.dispatch_table 將類對映到函式)。雖然返回值被完全相同地解釋,我們將它們統稱為 __reduce__。
重要提示: pickling 舊式類例項時,不會查詢 __reduce__ 或 __reduce_ex__ 方法,也不會在 copy_reg 分派表中查詢 reduce 函式,因此舊式類無法在此意義上提供 __reduce__ 功能。舊式類必須使用 __getinitargs__ 和/或 __getstate__ 來自定義 pickling。這些將在下面介紹。
__reduce__ 必須返回一個字串或一個元組。如果返回一個字串,則表示一個物件,其狀態不被 pickling,而是引用一個同等物件,該物件透過名稱進行引用。令人驚訝的是,__reduce__ 返回的字串應該是物件的本地名稱(相對於其模組);pickle 模組會搜尋模組名稱空間來確定物件的模組。
本節其餘部分將討論 __reduce__ 返回的元組。它是一個可變長度元組,長度為 2 到 5。前兩個項(函式和引數)是必需的。其餘項是可選的,可以從末尾省略;為可選項的值提供 None 與省略它們的效果相同。最後兩項是本 PEP 中新增的。各項的順序是:
| 函式 | 必需。 一個可呼叫物件(不一定是函式),用於建立物件的初始版本;稍後可以向物件新增狀態以完全重建 pickled 狀態。此函式本身必須是可 pickling 的。有關特殊情況(本 PEP 新增),請參閱 |
| 引數 | 必需。 一個元組,給出函式的引數列表。作為一種特殊情況,專為 Zope 2 的 |
Unpickling 呼叫 function(*arguments) 來建立一個初始物件,下面稱為 *obj*。如果省略了其餘項,則 unpickling 對此物件的處理就結束了,*obj* 就是結果。否則,*obj* 在 unpickling 時會根據指定的每一項進行修改,如下所示。
| 狀態 | 可選。 附加狀態。如果此項不為 obj.__dict__.update(state)
或者,如果 for k, v in state.items():
setattr(obj, k, v)
|
| 列表項 | 可選,並且是本 PEP 新增的。 如果此項不為 |
| 字典項 | 可選,並且是本 PEP 新增的。 如果此項不為 |
注意:在 Python 2.2 及更早版本中,使用 cPickle 時,如果狀態存在,即使它是 None 也會被 pickling;避免 __setstate__ 呼叫唯一安全的方法是從 __reduce__ 返回一個二元元組。(但 pickle.py 不會 pickling None 狀態。)在 Python 2.3 中,當 __reduce__ 在 pickling 時返回值為 None 的狀態時,__setstate__ 在 unpickling 時永遠不會被呼叫。
一個 __reduce__ 實現,如果需要同時在 Python 2.2 和 Python 2.3 下工作,可以檢查變數 pickle.format_version 來確定是使用 *listitems* 和 *dictitems* 功能。如果此值 >= "2.0",則支援它們。如果不支援,則任何列表或字典項都應以某種方式包含在“state”返回值中,並且 __setstate__ 方法應準備好接受列表或字典項作為狀態的一部分(如何實現取決於應用程式)。
__reduce_ex__ API
有時瞭解協議版本對於實現 __reduce__ 很有用。這可以透過實現一個名為 __reduce_ex__ 的方法而不是 __reduce__ 來完成。__reduce_ex__ 存在時,優先於 __reduce__ 呼叫(為向後相容,您仍然可以提供 __reduce__)。__reduce_ex__ 方法將使用一個整數引數呼叫,即協議版本。
‘object’類同時實現了 __reduce__ 和 __reduce_ex__;然而,如果子類覆蓋了 __reduce__ 但沒有覆蓋 __reduce_ex__,__reduce_ex__ 實現會檢測到這種情況並呼叫 __reduce__。
在沒有 __reduce__ 實現的情況下自定義 pickling
如果對於某個類沒有可用的 __reduce__ 實現,則需要分別考慮三種情況,因為它們的處理方式不同:
- 舊式類例項,所有協議
- 新式類例項,協議 0 和 1
- 新式類例項,協議 2
用 C 實現的型別被視為新式類。但是,除了常見的內建型別外,這些型別需要提供 __reduce__ 實現才能使用協議 0 或 1 進行 pickling。協議 2 還支援使用 __getnewargs__、__getstate__ 和 __setstate__ 的內建型別。
情況 1:pickling 舊式類例項
這種情況與所有協議都相同,並且自 Python 2.1 起未發生變化。
對於舊式類,不使用 __reduce__。相反,舊式類可以透過提供名為 __getstate__、__setstate__ 和 __getinitargs__ 的方法來定製其 pickling。如果沒有這些方法,將實現舊式類例項的預設 pickling 策略,只要所有例項變數都可以 pickling,該策略就能正常工作。此預設策略已透過 __getstate__ 和 __setstate__ 的預設實現來記錄。
自定義舊式類例項 pickling 的主要方法是指定 __getstate__ 和/或 __setstate__ 方法。如果一個類實現了其中一個而不是另一個,只要它與預設版本相容,都是可以的。
__getstate__ 方法
__getstate__ 方法應返回一個可 pickling 的值,該值表示物件的狀態,但不引用物件本身。如果不存在 __getstate__ 方法,則使用一個預設實現,該實現返回 self.__dict__。
__setstate__ 方法
__setstate__ 方法應接受一個引數;在 unpickling 時,它將使用 __getstate__ 的返回值(或其預設實現)來呼叫。
如果不存在 __setstate__ 方法,則提供一個預設實現,該實現假定狀態是一個將例項變數名對映到值的字典。預設實現嘗試兩種操作:
- 首先,它嘗試呼叫
self.__dict__.update(state)。 - 如果
update()呼叫因RuntimeError異常而失敗,它將為狀態字典中的每個(key, value)對呼叫setattr(self, key, value)。這僅在受限制的執行模式下(請參閱rexec標準庫模組)unpickling 時發生。
__getinitargs__ 方法
__setstate__ 方法(或其預設實現)要求一個新的物件已經存在,以便可以呼叫其 __setstate__ 方法。關鍵是建立一個尚未完全初始化的新物件;特別是,如果可能,不應呼叫類的 __init__ 方法。
以下是可能的方案:
- 通常,使用以下技巧:建立一個簡單的舊式類(沒有方法或例項變數)的例項,然後使用
__class__賦值將其類更改為所需的類。這會建立一個所需類的例項,該例項具有一個空的__dict__,且其__init__未被呼叫。 - 但是,如果類有一個名為
__getinitargs__的方法,則不使用上述技巧,而是透過使用__getinitargs__返回的元組作為類建構函式的引數列表來建立類例項。即使__getinitargs__返回一個空元組,也會這樣做——一個返回()的__getinitargs__方法並不等同於根本沒有__getinitargs__。__getinitargs__*必須* 返回一個元組。 - 在受限制的執行模式下,第一個專案符號中的技巧不起作用;在這種情況下,如果不存在
__getinitargs__方法,類建構函式將使用空引數列表呼叫。這意味著,為了使舊式類在受限制的執行模式下能夠 unpickle,它必須實現__getinitargs__,或者它的建構函式(即__init__方法)必須能夠無引數呼叫。
情況 2:使用協議 0 或 1 pickling 新式類例項
這種情況與 Python 2.2 相同。為了在不考慮向後相容性時更好地 pickling 新式類例項,應使用協議 2;請參閱下面的情況 3。
新式類,無論是用 C 還是 Python 實現,都從通用基類‘object’繼承預設的 __reduce__ 實現。
對於 pickle 模組內建支援的內建型別,不使用此預設 __reduce__ 實現。以下是這些型別的完整列表:
- 具體內建型別:
NoneType、bool、int、float、complex、str、unicode、tuple、list、dict。(複數透過在copy_reg中註冊的__reduce__實現來支援。)在 Jython 中,PyStringMap也包含在此列表中。 - 舊式例項。
- 舊式類物件、Python 函式物件、內建函式和方法物件以及新式型別物件(== 新式類物件)。這些物件按名稱 pickling,而不是按值 pickling:在 unpickling 時,會替換一個具有相同名稱(包括包名稱的完全限定模組名稱以及該模組中的變數名稱)的物件的引用。
對於上面未提及的內建型別以及用 C 實現的新式類,預設 __reduce__ 實現將在 pickling 時失敗:如果它們需要可 pickling,則必須在協議 0 和 1 下提供自定義 __reduce__ 實現。
對於用 Python 實現的新式類,預設 __reduce__ 實現(copy_reg._reduce)工作方式如下:
設 D 是要 pickling 的物件所屬的類。首先,找到最近的用 C 實現的基類(無論是內建型別還是擴充套件類定義的型別)。稱此基類為 B,並將要 pickling 的物件類命名為 D。除非 B 是‘object’類,否則 B 類的例項必須是可 pickling 的,方法是擁有內建支援(如上面三個專案符號點中所述),或者擁有一個非預設的 __reduce__ 實現。B 不能與 D 是同一個類(如果是,則意味著 D 不是用 Python 實現的)。
預設 __reduce__ 生成的可呼叫物件是 copy_reg._reconstructor,其引數元組是 (D, B, basestate),其中 basestate 是 None(如果 B 是內建 object 類),而 basestate 是
basestate = B(obj)
如果 B 不是內建 object 類。這適用於 pickling 內建型別的子類,例如,list(some_list_subclass_instance) 會生成 list 子類例項的“列表部分”。
物件在 unpickling 時由 copy_reg._reconstructor 重新建立,如下所示:
obj = B.__new__(D, basestate)
B.__init__(obj, basestate)
使用預設 __reduce__ 實現的物件可以透過定義 __getstate__ 和/或 __setstate__ 方法來自定義。這些方法的工作方式與上面為舊式類描述的幾乎相同,不同之處在於,如果 __getstate__ 返回一個其值被視為 false 的物件(例如 None,或值為零的數字,或空序列或對映),則此狀態不會被 pickling,並且 __setstate__ 在 unpickling 時根本不會被呼叫。如果 __getstate__ 存在並返回一個 true 值,該值將成為預設 __reduce__ 返回的元組的第三個元素,並在 unpickling 時將該值傳遞給 __setstate__。如果 __getstate__ 不存在,但 obj.__dict__ 存在,那麼 obj.__dict__ 將成為預設 __reduce__ 返回的元組的第三個元素,同樣在 unpickling 時,該值將被傳遞給 obj.__setstate__。預設的 __setstate__ 與舊式類的預設 __setstate__ 相同,如上所述。
請注意,此策略會忽略 slots。具有 slots 但沒有 __getstate__ 方法的新式類例項不能使用協議 0 和 1 進行 pickling;程式碼會顯式檢查這種情況。
請注意,pickling 新式類例項時會忽略 __getinitargs__(如果存在,並且在所有協議下)。 __getinitargs__ 只對舊式類有用。
情況 3:使用協議 2 pickling 新式類例項
在協議 2 下,預設的從‘object’基類繼承的 __reduce__ 實現被*忽略*。相反,使用了一個不同的預設實現,該實現允許比協議 0 或 1 更有效地 pickling 新式類例項,但以犧牲與 Python 2.2 的向後相容性為代價(這意味著最多是協議 2 的 pickle 在 Python 2.3 之前無法 unpickle)。
自定義使用三個特殊方法:__getstate__、__setstate__ 和 __getnewargs__(請注意,__getinitargs__ 再次被忽略)。如果一個類實現了一個或多個而不是全部這些方法,只要它與預設實現相容,都是可以的。
__getstate__ 方法
__getstate__ 方法應返回一個可 pickling 的值,該值表示物件的狀態,但不引用物件本身。如果不存在 __getstate__ 方法,則使用一個下面介紹的預設實現。
這裡有一個微妙的區別:對於舊式類和新式類:如果一箇舊式類的 __getstate__ 返回 None,那麼 self.__setstate__(None) 將在 unpickling 時被呼叫。但是,如果一個新式類的 __getstate__ 返回 None,那麼它的 __setstate__ 在 unpickling 時根本不會被呼叫。
如果不存在 __getstate__ 方法,則計算一個預設狀態。有幾種情況:
- 對於沒有例項
__dict__且沒有__slots__的新式類,預設狀態為None。 - 對於有一個例項
__dict__且沒有__slots__的新式類,預設狀態為self.__dict__。 - 對於有一個例項
__dict__和__slots__的新式類,預設狀態是包含兩個字典的元組:self.__dict__,以及一個將 slot 名稱對映到 slot 值的字典。只有具有值的 slot 才包含在後者中。 - 對於有
__slots__且沒有例項__dict__的新式類,預設狀態是其第一個元素為None,第二個元素為前一個專案符號中描述的將 slot 名稱對映到 slot 值的字典的元組。
__setstate__ 方法
__setstate__ 方法應接受一個引數;在 unpickling 時,它將使用 __getstate__ 返回的值或上面描述的預設狀態(如果未定義 __getstate__ 方法)來呼叫。
如果不存在 __setstate__ 方法,則提供一個預設實現,該實現可以處理上面描述的預設 __getstate__ 返回的狀態。
__getnewargs__ 方法
與舊式類一樣,__setstate__ 方法(或其預設實現)要求一個新的物件已經存在,以便可以呼叫其 __setstate__ 方法。
在協議 2 中,使用了一個新的 pickling 操作碼,該操作碼導致新物件的建立方式如下:
obj = C.__new__(C, *args)
其中 C 是 pickled 物件的類,而 args 要麼是空元組,要麼是 __getnewargs__ 方法返回的元組(如果已定義)。__getnewargs__ 必須返回一個元組。不存在 __getnewargs__ 方法等同於存在一個返回 () 的方法。
__newobj__ unpickling 函式
當 __reduce__ 返回的 unpickling 函式(返回元組的第一個元素)名稱為 __newobj__ 時,對於 pickle 協議 2 會發生特殊情況。一個名為 __newobj__ 的 unpickling 函式假定具有以下語義:
def __newobj__(cls, *args):
return cls.__new__(cls, *args)
pickle 協議 2 特殊處理一個具有此名稱的 unpickling 函式,併發出一個 pickling 操作碼,該操作碼接收‘cls’和‘args’,將返回 cls.__new__(cls, *args),而無需 pickling __newobj__ 的引用(這是協議 2 為新式類例項在沒有 __reduce__ 實現時使用的相同 pickling 操作碼)。這也是協議 2 pickle 比舊式 pickle 小得多的主要原因。當然,pickling 程式碼無法驗證名為 __newobj__ 的函式是否具有預期的語義。如果您使用的 unpickling 函式名為 __newobj__ 但返回不同的內容,那麼您將自食其果。
在 Python 2.2 下使用此功能是安全的;__newobj__ 的推薦實現中沒有任何內容依賴於 Python 2.3。
擴充套件登錄檔
協議 2 支援一種新的機制來減小 pickle 的大小。
當 pickling 類例項(舊式或新式)時,類的完整名稱(包括包名稱的模組名稱和類名稱)將被包含在 pickle 中。特別對於生成許多小型 pickle 的應用程式來說,這會產生大量開銷,並且必須在每個 pickle 中重複。對於大型 pickle,在使用協議 1 時,對同一類名的重複引用會使用“memo”功能進行壓縮;但每個類名必須在每個 pickle 中至少完整拼寫一次,這會導致小型 pickle 的開銷很大。
擴充套件登錄檔允許用小的整數來表示最常用的名稱,這些整數的 pickling 非常高效:程式碼範圍在 1-255 的擴充套件碼只需要兩個位元組(包括操作碼),範圍在 256-65535 的擴充套件碼只需要三個位元組(包括操作碼)。
pickle 協議的設計目標之一是使 pickle “無上下文”:只要您安裝了包含 pickle 所引用的類的模組,就可以 unpickle 它,而無需提前匯入任何這些類。
無限制地使用擴充套件碼可能會危及 pickle 的這一理想屬性。因此,擴充套件碼的主要用途保留給將由某個標準化機構標準化的編碼集。鑑於這是 Python,標準化機構是 PSF。PSF 將不時地決定一個對映表,將擴充套件碼對映到類名(或偶爾的其他全域性物件名稱;函式也符合資格)。此表將包含在下一個 Python 釋出版本中。
但是,對於某些應用程式,例如 Zope,無上下文 pickle 不是必需的,並且等待 PSF 標準化某些編碼可能不切實際。為這類應用程式提供了兩種解決方案。
首先,保留了一些擴充套件碼範圍供私人使用。任何應用程式都可以在這些範圍內註冊編碼。在這些範圍內交換使用編碼的兩個應用程式需要某種帶外機制來就擴充套件碼與名稱之間的對映達成一致。
其次,一些大型 Python 專案(例如 Zope)可以被分配一個“私人使用”範圍之外的擴充套件碼範圍,它們可以根據需要進行分配。
擴充套件登錄檔定義為擴充套件碼與名稱之間的對映。當 unpickle 一個擴充套件碼時,它最終會產生一個物件,但這個物件是透過將名稱解釋為模組名稱後跟類(或函式)名稱來獲取的。名稱到物件的對映被快取。某些名稱可能無法匯入;只要沒有包含對這些名稱引用的 pickle 需要 unpickle,這就不成問題。(對於協議 0 或 1 的 pickle 中的這些名稱的直接引用,已經存在同樣的問題。)
以下是擬議的擴充套件碼範圍的初始分配:
| 第一個 | 最後一個 | 計數 | 用途 |
|---|---|---|---|
| 0 | 0 | 1 | 保留—將永遠不會使用 |
| 1 | 127 | 127 | 保留給 Python 標準庫 |
| 128 | 191 | 64 | 保留給 Zope |
| 192 | 239 | 48 | 保留給第三方 |
| 240 | 255 | 16 | 保留供私人使用(將永遠不會分配) |
| 256 | MAX | MAX | 保留供將來分配 |
*MAX* 代表 2147483647,或 2**31-1。這是當前定義的協議的硬限制。
目前,還沒有分配任何特定的擴充套件碼。
擴充套件登錄檔 API
擴充套件登錄檔在 copy_reg 模組中維護為私有全域性變數。該模組定義了以下三個函式來操作登錄檔:
add_extension(module, name, code)- 註冊一個擴充套件碼。*module* 和 *name* 引數必須是字串;*code* 必須是一個
int,取值範圍在 1 到 *MAX* 之間(含)。這必須要麼將新的(module, name)對註冊到一個新編碼,要麼是之前未被remove_extension()呼叫取消的先前呼叫的冗餘重複;一個(module, name)對不能對映到多個編碼,一個編碼也不能對映到多個(module, name)對。 remove_extension(module, name, code)- 引數與
add_extension()相同。刪除先前註冊的(module, name)與 *code* 之間的對映。 clear_extension_cache()- 擴充套件碼的實現可以使用快取來加速載入頻繁命名的物件。可以透過呼叫此方法來清空此快取(移除對快取物件的引用)。
請注意,API 不強制執行標準範圍分配。應用程式應自行遵守。
copy 模組
傳統上,copy 模組支援 pickling API 的擴充套件子集,用於自定義 copy() 和 deepcopy() 操作。
特別是,除了檢查 __copy__ 或 __deepcopy__ 方法外,copy() 和 deepcopy() 始終查詢 __reduce__,對於舊式類,則查詢 __getinitargs__、__getstate__ 和 __setstate__。
在 Python 2.2 中,從‘object’繼承的預設 __reduce__ 實現使得複製簡單的新式類成為可能,但 slots 和各種其他特殊情況未被涵蓋。
在 Python 2.3 中,對 copy 模組進行了幾項更改:
__reduce_ex__被支援(並且始終使用 2 作為協議版本引數呼叫)。- 支援
__reduce__的四引數和五引數返回值。 - 在查詢
__reduce__方法之前,會像 pickling 一樣查詢copy_reg.dispatch_table。 - 當
__reduce__方法從 object 繼承時,它會被(無條件地)替換為一個更好的實現,該實現使用與 pickle 協議 2 相同的 API:__getnewargs__、__getstate__和__setstate__,並處理list和dict子類,以及處理 slots。
作為後一項更改的結果,某些在 Python 2.2 下可以複製的新式類在 Python 2.3 下將不再可複製。(這些類也無法使用 pickle 協議 2 進行 pickling。)此類的一個最小示例:
class C(object):
def __new__(cls, a):
return object.__new__(cls)
問題僅在覆蓋 __new__ 並且除了類引數外至少有一個強制引數時才會發生。
為了解決這個問題,應新增一個 __getnewargs__ 方法,該方法返回適當的引數元組(不包括類)。
Pickling Python 長整數
在協議 0 和 1 中,pickling 和 unpickling Python 長整數所需時間與數字位數成二次方關係。在協議 2 下,新的操作碼支援長整數的線性時間 pickling 和 unpickling。
Pickling 布林值
協議 2 引入了新的操作碼,用於直接 pickling True 和 False。在協議 0 和 1 下,布林值被 pickling 為整數,利用 pickle 中整數表示的技巧,以便 unpickler 可以識別出意圖是布林值。該技巧每個 pickling 的布林值消耗 4 位元組。新的布林操作碼每個布林值消耗 1 位元組。
Pickling 小元組
協議 2 引入了新的操作碼,用於更緊湊地 pickling 長度為 1、2 和 3 的元組。協議 1 以前引入了一個操作碼,用於更緊湊地 pickling 空元組。
協議識別
協議 2 引入了一個新的操作碼,所有協議 2 pickle 都以此開頭,指示該 pickle 是協議 2。因此,在舊版本 Python 中嘗試 unpickle 協議 2 pickle 將立即引發“未知操作碼”異常。
Pickling 大列表和字典
協議 1 將大列表和字典“一次性”pickling,從而最大程度地減小 pickle 大小,但這要求 unpickling 建立一個與被 unpickling 物件一樣大的臨時物件。協議 2 的部分更改將大列表和字典分解為每個不超過 1000 個元素的小塊,這樣 unpickling 就不必建立大於容納 1000 個元素所需大小的臨時物件。然而,這並非協議 2 的一部分:生成的操作碼仍屬於協議 1。__reduce__ 實現返回可選的新列表項或字典項迭代器也受益於此 unpickling 臨時空間最佳化。
版權
本文件已置於公共領域。
來源:https://github.com/python/peps/blob/main/peps/pep-0307.rst