Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

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 版本中也有效,因此可以用来选择最高可用协议,且兼顾向后和向前兼容。此外,picklecPickle 模块都提供了一个新的模块常量 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 新增),请参阅 __newobj__ 部分。

参数 必需。

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

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

状态 可选。

附加状态。如果此项不为 None,则会对状态进行 pickling,并在 unpickling 时调用 obj.__setstate__(state)。如果未定义 __setstate__ 方法,则会提供一个默认实现,该实现假定状态是一个将实例变量名映射到其值的字典。默认实现调用

obj.__dict__.update(state)

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

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

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

字典项 可选,并且是本 PEP 新增的。

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

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

  1. 旧式类实例,所有协议
  2. 新式类实例,协议 0 和 1
  3. 新式类实例,协议 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__ 实现。以下是这些类型的完整列表:

  • 具体内置类型:NoneTypeboolintfloatcomplexstrunicodetuplelistdict。(复数通过在 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),其中 basestateNone(如果 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__,并处理 listdict 子类,以及处理 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 TrueFalse。在协议 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

最后修改:2025-02-01 08:59:27 GMT