Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 增强提案

PEP 307 – pickle 协议扩展

作者:
Guido van Rossum, Tim Peters
状态:
最终
类型:
标准跟踪
创建:
2003年1月31日
Python 版本:
2.3
历史记录:
2003年2月7日

目录

简介

在 Python 2.2 中,对新式对象的 pickling 方式有些笨拙,并且与经典类实例相比会导致 pickle 大小膨胀。本 PEP 文档记录了 Python 2.3 中的一个新的 pickle 协议,它解决了这个问题以及许多其他 pickle 问题。

指定新的 pickle 协议有两个方面:必须指定构成 pickled 数据的字节流,并且必须指定对象与 pickling 和 unpickling 引擎之间的接口。本 PEP 专注于 API 问题,尽管它可能会偶尔涉及字节流格式的细节以证明某个选择的合理性。pickle 字节流格式由标准库模块 pickletools.py 正式记录(已签入 Python 2.3 的 CVS)。

本 PEP 试图全面记录 pickled 对象与 pickling 过程之间的接口,通过指定“本 PEP 中的新增内容”来突出新增内容。(除了对指定 pickling 协议给 picklers 的 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__ 来实现 picklable。经过充分考虑,我们得出结论,减少新式对象 pickle 大小的唯一方法是向 pickle 协议添加新的操作码。最终结果是,使用新的协议,上述示例中的 pickle 大小为 35(开头使用两个额外的字节来指示协议版本,尽管这并非严格必要)。

协议版本

以前,pickling(但不是 unpickling)区分文本模式和二进制模式。根据设计,二进制模式是文本模式的超集,unpicklers 不需要预先知道传入的 pickle 使用的是文本模式还是二进制模式。用于 unpickling 的虚拟机与模式无关;某些操作码在文本模式下 simply 不使用。

追溯地,文本模式现在称为协议 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 版本中也能正常工作,因此可以用来以向后和向前兼容的方式选择可用的最高协议。此外,picklecPickle 都提供了一个新的模块常量 HIGHEST_PROTOCOL,等于模块可以读取的最高协议号。这比传递 -1 更简洁,但在 Python 2.3 之前无法使用。

The 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 实现如果没有更大的时间投入, simply 无法做出这样的保证。因此,从 Python 2.3 开始,所有关于 unpickling 的安全检查都被正式删除,并替换为以下警告

警告

不要 unpickle 来自不可信或未经身份验证的来源的数据。

即使存在安全检查,此警告也适用于以前的 Python 版本。

扩展的 __reduce__ API

类可以使用多个 API 来控制 pickling。也许最流行的是 __getstate____setstate__;但最强大的一个是 __reduce__。(还有 __getinitargs__,我们将在下面添加 __getnewargs__。)

提供 __reduce__ 功能有几种方法:类可以实现 __reduce__ 方法或 __reduce_ex__ 方法(参见下一节),或者可以在 copy_reg 中声明一个 reduce 函数(copy_reg.dispatch_table 将类映射到函数)。但是,返回值的解释完全相同,我们将统称为 __reduce__

重要:经典类实例的 pickling 不会查找 copy_reg 分派表中的 __reduce____reduce_ex__ 方法或 reduce 函数,因此经典类无法以此处意图的方式提供 __reduce__ 功能。经典类必须使用 __getinitargs__ 和/或 __getstate__ 来自定义 pickling。这些将在下面描述。

__reduce__ 必须返回字符串或元组。如果它返回一个字符串,则此字符串是一个其状态无需 pickled 的对象,而是对由名称引用的等效对象的引用。令人惊讶的是,__reduce__ 返回的字符串应该是对象的本地名称(相对于其模块);pickle 模块搜索模块命名空间以确定对象的模块。

本节的其余部分与 __reduce__ 返回的元组有关。它是一个可变大小的元组,长度为 2 到 5。前两个项目(函数和参数)是必需的。其余项目是可选的,可以从末尾省略;为可选项目的 value 指定 None 的效果与省略它相同。最后两个项目是本 PEP 中的新增内容。这些项目按顺序排列

函数 必需的。

一个可调用对象(不一定是函数),用于创建对象的初始版本;稍后可以向对象添加状态以完全重建 pickled 状态。此函数本身必须是 picklable 的。有关此处的一个特殊情况(本 PEP 中的新增内容),请参见关于 __newobj__ 的部分。

参数 必需的。

一个元组,给出函数的参数列表。作为一个特殊情况,专为 Zope 2 的 ExtensionClass 设计,它可以是 None;在这种情况下,函数应该是一个类或类型,并且 function.__basicnew__() 被调用以创建对象的初始版本。此例外已弃用。

Unpickling 调用 function(*arguments) 来创建一个初始对象,在下面称为 *obj*。如果省略了其余项目,则此对象的 unpickling 到此结束,*obj* 就是结果。否则,在 unpickling 时,每个指定的项目都会修改 *obj*,如下所示。

状态 可选的。

附加状态。如果它不是 None,则状态会被 pickled,并且在 unpickling 时会调用 obj.__setstate__(state)。如果没有定义 __setstate__ 方法,则会提供一个默认实现,它假设状态是一个字典,将实例变量名称映射到其值。默认实现调用

obj.__dict__.update(state)

或者,如果 update() 调用失败,则

for k, v in state.items():
    setattr(obj, k, v)
列表项 可选的,并且是本 PEP 中的新增内容。

如果该值不为None,则它应该是一个迭代器(而不是序列!),生成连续的列表项。这些列表项将被pickle序列化,并使用obj.append(item)obj.extend(list_of_items)追加到对象中。这主要用于list的子类,但只要其他类具有具有适当签名的append()extend()方法,也可以使用。 (使用append()还是extend()取决于使用的pickle协议版本以及要追加的项数,因此两者都必须支持。)

dictitems 可选的,并且是本 PEP 中的新增内容。

如果该值不为None,则它应该是一个迭代器(而不是序列!),生成连续的字典项,这些项应该是(key, value)形式的元组。这些项将被pickle序列化,并使用obj[key] = value存储到对象中。这主要用于dict的子类,但只要其他类实现了__setitem__,也可以使用。

注意:在Python 2.2及之前版本中,当使用cPickle时,即使状态为None,也会被pickle序列化;避免__setstate__调用的唯一安全方法是从__reduce__返回一个二元组。(但是pickle.py如果状态为None,则不会将其序列化。)在Python 2.3中,当__reduce__在序列化时返回值为None的状态时,在反序列化时永远不会调用__setstate__

需要同时在Python 2.2和Python 2.3下工作的__reduce__实现可以检查变量pickle.format_version以确定是否使用listitemsdictitems功能。如果该值为>= "2.0",则表示支持。否则,任何列表或字典项都应该以某种方式合并到'state'返回值中,并且__setstate__方法应该准备接受列表或字典项作为状态的一部分(如何执行此操作取决于应用程序)。

The __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__实现,则需要分别考虑三种情况,因为它们处理方式不同

  1. 经典类实例,所有协议
  2. 新式类实例,协议0和1
  3. 新式类实例,协议2

用C实现的类型被认为是新式类。但是,除了常见的内置类型外,这些类型需要提供__reduce__实现才能使用协议0或1进行pickle序列化。协议2支持内置类型提供__getnewargs____getstate____setstate__

案例 1:pickling 经典类实例

这种情况对于所有协议都是相同的,并且与Python 2.1相比没有变化。

对于经典类,不使用__reduce__。相反,经典类可以通过提供名为__getstate____setstate____getinitargs__的方法来自定义其pickle序列化。在没有这些方法的情况下,将实现经典类实例的默认pickle序列化策略,只要所有实例变量都是可pickle序列化的,该策略就可以正常工作。此默认策略在__getstate____setstate__的默认实现方面进行了说明。

自定义经典类实例pickle序列化的主要方法是指定__getstate__和/或__setstate__方法。如果一个类实现其中一个但没有实现另一个,只要它与默认版本兼容,就可以了。

The __getstate__ 方法

__getstate__方法应该返回一个可pickle序列化的值,表示对象的状态,而不引用对象本身。如果没有__getstate__方法,则使用返回self.__dict__的默认实现。

The __setstate__ 方法

__setstate__方法应该接受一个参数;它将使用__getstate__(或其默认实现)返回的值进行调用。

如果没有__setstate__方法,则提供一个默认实现,该实现假设状态是一个字典,将实例变量名称映射到值。默认实现尝试两件事

  • 首先,它尝试调用self.__dict__.update(state)
  • 如果update()调用因RuntimeError异常而失败,则对于状态字典中的每个(key, value)对,它都会调用setattr(self, key, value)。这仅在受限执行模式下反序列化时发生(请参阅rexec标准库模块)。

The __getinitargs__ 方法

__setstate__方法(或其默认实现)要求已经存在一个新对象,以便可以调用其__setstate__方法。关键是要创建一个尚未完全初始化的新对象;特别是,如果可能,不应调用类的__init__方法。

这些是可能性

  • 通常,使用以下技巧:创建一个简单经典类的实例(一个没有任何方法或实例变量的类),然后使用__class__赋值将其类更改为所需的类。这将创建一个所需类的实例,该实例具有一个空的__dict__,并且尚未调用其__init__
  • 但是,如果类具有名为__getinitargs__的方法,则不使用上述技巧,而是通过使用__getinitargs__返回的元组作为参数列表传递给类构造函数来创建类实例。即使__getinitargs__返回一个空元组,也会这样做——返回()__getinitargs__方法不等于根本没有__getinitargs____getinitargs__必须返回一个元组。
  • 在受限执行模式下,第一个要点中的技巧不起作用;在这种情况下,如果不存在__getinitargs__方法,则将使用空参数列表调用类构造函数。这意味着,为了使经典类在受限执行模式下可pickle序列化,它必须实现__getinitargs__或其构造函数(即其__init__方法)必须可以在没有参数的情况下被调用。

案例 2:使用协议 0 或 1 pickling 新式类实例

这种情况与Python 2.2相比没有变化。为了在向后兼容性不是问题时更好地对新式类实例进行pickle序列化,应该使用协议2;请参见下面的情况3。

无论是在C中还是在Python中实现的新式类,都从通用基类'object'继承默认的__reduce__实现。

此默认__reduce__实现不适用于pickle模块具有内置支持的那些内置类型。以下是这些类型的完整列表

  • 具体内置类型:NoneTypeboolintfloatcomplexstrunicodetuplelistdict。(Complex通过在copy_reg中注册的__reduce__实现得到支持。)在Jython中,PyStringMap也包含在此列表中。
  • 经典实例。
  • 经典类对象、Python 函数对象、内置函数和方法对象,以及新式类型对象(== 新式类对象)。这些对象是通过名称而不是值进行 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 如果 B 是内置对象类,则为 None,如果 B 不是内置对象类,则 basestate

basestate = B(obj)

如果 B 不是内置对象类。这适用于 pickling 内置类型的子类,例如,list(some_list_subclass_instance) 生成 list 子类实例的“列表部分”。

在 unpickling 时,对象由 copy_reg._reconstructor 重新创建,如下所示:

obj = B.__new__(D, basestate)
B.__init__(obj, basestate)

使用默认 __reduce__ 实现的对象可以通过定义 __getstate__ 和/或 __setstate__ 方法来自定义它。这些方法的工作方式与上面为经典类描述的几乎相同,除了如果 __getstate__ 返回一个其值为假(例如 None、值为零的数字或空序列或映射)的对象,则不会对该状态进行 pickling,并且根本不会调用 __setstate__。如果 __getstate__ 存在并返回真值,则该值成为默认 __reduce__ 返回的元组的第三个元素,并且在 unpickling 时,该值将传递给 __setstate__。如果 __getstate__ 不存在,但 obj.__dict__ 存在,则 obj.__dict__ 成为默认 __reduce__ 返回的元组的第三个元素,并且在 unpickling 时,该值将传递给 obj.__setstate__。默认的 __setstate__ 与上面描述的经典类的相同。

请注意,此策略忽略了槽。具有槽但没有 __getstate__ 方法的新式类的实例不能由协议 0 和 1 进行 pickling;代码明确检查了此条件。

请注意,如果存在(并且在所有协议下),pickling 新式类实例会忽略 __getinitargs____getinitargs__ 仅对经典类有用。

案例 3:使用协议 2 pickling 新式类实例

在协议 2 下,从“object”基类继承的默认 __reduce__ 实现被 *忽略*。相反,使用不同的默认实现,这使得新式类实例的 pickling 比协议 0 或 1 更有效率,但代价是与 Python 2.2 不兼容(这意味着 Python 2.3 之前的版本无法解开协议 2 的 pickle)。

自定义使用三个特殊方法:__getstate____setstate____getnewargs__(请注意,__getinitargs__ 再次被忽略)。如果一个类实现了一个或多个但不是所有这些方法,只要它与默认实现兼容,就可以。

The __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__ 和一个将槽名称映射到槽值的字典。只有具有值的槽才会包含在后者中。
  • 对于具有 __slots__ 且没有实例 __dict__ 的新式类,默认状态是一个元组,其第一个项目是 None,其第二个项目是上一个要点中描述的将槽名称映射到槽值的字典。

The __setstate__ 方法

__setstate__ 方法应该接受一个参数;它将使用 __getstate__ 返回的值或上面描述的默认状态(如果未定义 __getstate__ 方法)进行调用。

如果不存在 __setstate__ 方法,则提供一个默认实现,该实现可以处理上面描述的默认 __getstate__ 返回的状态。

The __getnewargs__ 方法

与经典类一样,__setstate__ 方法(或其默认实现)要求已经存在一个新对象,以便可以调用其 __setstate__ 方法。

在协议 2 中,使用一个新的 pickling 操作码,该操作码导致新对象的创建方式如下:

obj = C.__new__(C, *args)

其中 C 是 pickling 对象的类,args 或者是空元组,或者是 __getnewargs__ 方法返回的元组(如果已定义)。__getnewargs__ 必须返回一个元组。缺少 __getnewargs__ 方法等同于存在返回 () 的方法。

The __newobj__ unpickling 函数

__reduce__ 返回的可调用对象(返回的元组的第一个项目)的名称为 __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__ 的函数是否确实具有预期的语义。如果您使用名为 __newobj__ 的 unpickling 函数返回其他内容,则您将自食其果。

在 Python 2.2 下安全使用此功能;在 __newobj__ 的推荐实现中,没有任何内容依赖于 Python 2.3。

扩展注册表

协议 2 支持一种新的机制来减小 pickle 的大小。

当 pickling 类实例(经典或新式)时,类的完整名称(包括包名的模块名称和类名称)包含在 pickle 中。特别是对于生成许多小型 pickle 的应用程序,这会带来很多开销,这些开销必须在每个 pickle 中重复。对于大型 pickle,在使用协议 1 时,对相同类名的重复引用使用“备忘录”功能进行压缩;但每个类名必须至少在每个 pickle 中完整拼写一次,这会导致小型 pickle 产生大量开销。

扩展注册表允许使用小整数表示最常用的名称,这些整数的 pickling 非常高效:1-255 范围内的扩展代码只需要两个字节(包括操作码),256-65535 范围内的扩展代码只需要三个字节(包括操作码)。

pickle 协议的一个设计目标是使 pickle “上下文无关”:只要您安装了包含 pickle 引用的类的模块,您就可以对其进行解包,而无需提前导入任何这些类。

扩展代码的无节制使用可能会危及 pickle 的这一理想特性。因此,扩展代码的主要用途保留给由某些标准制定机构标准化的代码集。由于这是 Python,标准制定机构是 PSF。PSF 将不时决定一个将扩展代码映射到类名(或偶尔映射到其他全局对象名;函数也有资格)的表格。此表将并入下一个 Python 版本中。

但是,对于某些应用程序(如 Zope),上下文无关的 pickle 不是必需的,并且等待 PSF 标准化某些代码可能不切实际。针对此类应用程序提供了两种解决方案。

首先,保留了一些扩展代码范围供私用。任何应用程序都可以在这些范围内注册代码。使用这些范围内的代码交换 pickle 的两个应用程序需要某种带外机制来就扩展代码和名称之间的映射达成一致。

其次,可以为一些大型 Python 项目(例如 Zope)分配一个超出“私用”范围的扩展代码范围,它们可以根据需要分配这些代码。

扩展注册表被定义为扩展代码和名称之间的映射。当解包扩展代码时,它最终会生成一个对象,但此对象是通过将名称解释为模块名称后跟类(或函数)名称来获取的。从名称到对象的映射被缓存。某些名称可能无法导入,这是很有可能的;只要不必解包包含对这些名称的引用的任何 pickle,这就不应该成为问题。(对于使用协议 0 或 1 的 pickle 中对这些名称的直接引用,已经存在相同的问题。)

以下是建议的扩展代码范围的初始分配

起始 结束 数量 用途
0 0 1 保留 — 永远不会使用
1 127 127 保留用于 Python 标准库
128 191 64 保留用于 Zope
192 239 48 保留用于第三方
240 255 16 保留用于私用(永远不会分配)
256 最大值 最大值 保留用于将来分配

MAX 代表 2147483647,或 2**31-1。这是当前定义的协议的硬性限制。

目前,尚未分配任何特定的扩展代码。

扩展注册表 API

扩展注册表作为 copy_reg 模块中的私有全局变量维护。以下三个函数在此模块中定义以操作注册表

add_extension(module, name, code)
注册扩展代码。modulename 参数必须是字符串;code 必须是在 1 到 MAX(包含)范围内的 int。这必须将新的 (module, name) 对注册到新的代码,或者是对之前未被 remove_extension() 调用取消的调用的冗余重复;一个 (module, name) 对可能不会映射到多个代码,代码也可能不会映射到多个 (module, name) 对。
remove_extension(module, name, code)
参数与 add_extension() 的参数相同。删除先前注册的 (module, name)code 之间的映射。
clear_extension_cache()
扩展代码的实现可以使用缓存来加速加载频繁命名的对象。可以通过调用此方法清空此缓存(删除对缓存对象的引用)。

请注意,API 不会强制执行标准范围分配。应用程序有责任遵守这些分配。

The copy 模块

传统上,copy 模块支持 pickling API 的扩展子集,以自定义 copy()deepcopy() 操作。

特别是,除了检查 __copy____deepcopy__ 方法外,copy()deepcopy() 始终查找 __reduce__,并且对于经典类,查找 __getinitargs____getstate____setstate__

在 Python 2.2 中,从“object”继承的默认 __reduce__ 使复制简单的新的样式类成为可能,但槽和各种其他特殊情况没有涵盖。

在 Python 2.3 中,对 copy 模块进行了一些更改

  • 支持 __reduce_ex__(并且始终以 2 作为协议版本参数调用)。
  • 支持 __reduce__ 的四参数和五参数返回值。
  • 在查找 __reduce__ 方法之前,会查询 copy_reg.dispatch_table,就像 pickling 一样。
  • __reduce__ 方法从 object 继承时,它将(无条件地)替换为一个更好的方法,该方法使用与 pickle 协议 2 相同的 API:__getnewargs____getstate____setstate__,处理 listdict 子类,以及处理槽。

由于后者的更改,某些在 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 TrueFalse。在协议 0 和 1 下,布尔值被 pickling 为整数,在 pickle 中的整数表示中使用了一种技巧,以便解包器可以识别布尔值的意图。该技巧消耗了每个 pickling 的布尔值 4 个字节。新的布尔值操作码每个布尔值消耗 1 个字节。

Pickling 小元组

协议 2 引入了新的操作码,用于更紧凑地 pickling 长度为 1、2 和 3 的元组。协议 1 之前引入了用于更紧凑地 pickling 空元组的操作码。

协议识别

协议 2 引入了一个新的操作码,所有协议 2 pickle 都以此开头,以识别该 pickle 为协议 2。因此,尝试在旧版本的 Python 下解包协议 2 pickle 将立即引发“未知操作码”异常。

大型列表和字典的 Pickling

协议 1 pickle 大列表和字典“作为一个整体”,这最大程度地减少了 pickle 的大小,但需要解包创建与正在解包的对象一样大的临时对象。协议 2 更改的一部分将大型列表和字典分解成每个不超过 1000 个元素的部分,以便解包不需要创建大于保存 1000 个元素所需的临时对象。但是,这不是协议 2 的一部分:生成的 opcode 仍然是协议 1 的一部分。__reduce__ 返回可选的新 listitems 或 dictitems 迭代器的实现也受益于此解包临时空间优化。


来源:https://github.com/python/peps/blob/main/peps/pep-0307.rst

上次修改时间:2023-09-09 17:39:29 GMT