pickle — Python 对象序列化

源代码: Lib/pickle.py


pickle 模块实现了用于序列化和反序列化 Python 对象结构的二进制协议。 “Pickling” 是指将 Python 对象层次结构转换为字节流的过程,而 “unpickling” 是逆向操作,即通过字节流(来自二进制文件字节类对象)将其转换回对象层次结构。Pickling(和 unpickling)也称为 “序列化”、“封送处理”[1]或“扁平化”;然而,为了避免混淆,此处使用的术语是“pickling”和“unpickling”。

警告

pickle 模块不安全。仅反序列化您信任的数据。

可以构造恶意 pickle 数据,这些数据将在反序列化期间执行任意代码。切勿反序列化可能来自不可信来源或可能已被篡改的数据。

如果您需要确保数据未被篡改,请考虑使用 hmac 对数据进行签名。

如果您正在处理不可信数据,则更安全的序列化格式(例如 json)可能更合适。请参阅与 json 的比较

与其他 Python 模块的关系

marshal 的比较

Python 有一个更原始的序列化模块,名为 marshal,但通常情况下,pickle 始终是序列化 Python 对象的首选方式。marshal 主要用于支持 Python 的 .pyc 文件。

pickle 模块与 marshal 在几个重要方面有所不同

  • pickle 模块会跟踪它已经序列化的对象,这样对同一对象的后续引用就不会再次序列化。marshal 不会这样做。

    这对于递归对象和对象共享都有影响。递归对象是包含对自身引用的对象。Marshal 不处理这些对象,实际上,尝试封送递归对象会使您的 Python 解释器崩溃。当在要序列化的对象层次结构的不同位置有对同一对象的多个引用时,就会发生对象共享。pickle 只存储此类对象一次,并确保所有其他引用都指向主副本。共享对象保持共享,这对于可变对象来说可能非常重要。

  • marshal 不能用于序列化用户定义的类及其实例。pickle 可以透明地保存和恢复类实例,但类定义必须是可导入的,并且与对象存储时位于同一模块中。

  • marshal 序列化格式不保证在 Python 版本之间可移植。由于其主要作用是支持 .pyc 文件,Python 实现者保留在需要时以不向后兼容的方式更改序列化格式的权利。pickle 序列化格式保证在 Python 版本之间向后兼容,前提是选择了兼容的 pickle 协议,并且如果您的数据跨越了独特的重大更改语言边界,则 pickling 和 unpickling 代码会处理 Python 2 到 Python 3 的类型差异。

json 的比较

pickle 协议和 JSON (JavaScript Object Notation) 之间存在根本区别

  • JSON 是一种文本序列化格式(它输出 Unicode 文本,尽管大多数时候它被编码为 utf-8),而 pickle 是一种二进制序列化格式;

  • JSON 是人类可读的,而 pickle 不是;

  • JSON 具有互操作性,并在 Python 生态系统之外广泛使用,而 pickle 是 Python 特有的;

  • JSON 默认只能表示 Python 内置类型的一个子集,并且不能表示自定义类;pickle 可以表示极其大量的 Python 类型(其中许多是通过巧妙利用 Python 的自省功能自动实现的;复杂的情况可以通过实现特定的对象 API 来解决);

  • 与 pickle 不同,反序列化不受信任的 JSON 本身不会造成任意代码执行漏洞。

参见

json 模块:一个允许 JSON 序列化和反序列化的标准库模块。

数据流格式

pickle 使用的数据格式是 Python 特有的。这有一个优点,即不受 JSON 等外部标准的限制(JSON 无法表示指针共享);但是,这意味着非 Python 程序可能无法重构 pickled Python 对象。

默认情况下,pickle 数据格式使用相对紧凑的二进制表示。如果您需要最佳大小特性,您可以高效地压缩 pickled 数据。

模块 pickletools 包含用于分析 pickle 生成的数据流的工具。pickletools 源代码包含关于 pickle 协议使用的操作码的详细注释。

目前有 6 种不同的协议可用于 pickling。使用的协议版本越高,读取生成的 pickle 所需的 Python 版本就越新。

  • 协议版本 0 是最初的“人类可读”协议,并向后兼容早期版本的 Python。

  • 协议版本 1 是一种旧的二进制格式,也兼容早期版本的 Python。

  • 协议版本 2 在 Python 2.3 中引入。它提供了更高效的 新式类 pickling。有关协议 2 带来的改进信息,请参阅PEP 307

  • 协议版本 3 在 Python 3.0 中添加。它明确支持 bytes 对象,并且无法由 Python 2.x 反序列化。这是 Python 3.0–3.7 中的默认协议。

  • 协议版本 4 在 Python 3.4 中添加。它增加了对超大对象的支持、更多种类对象的 pickling 以及一些数据格式优化。这是 Python 3.8–3.13 中的默认协议。有关协议 4 带来的改进信息,请参阅PEP 3154

  • 协议版本 5 在 Python 3.8 中添加。它增加了对带外数据的支持,并加快了带内数据的速度。它是 Python 3.14 及更高版本中的默认协议。有关协议 5 带来的改进信息,请参阅PEP 574

备注

序列化是一个比持久化更原始的概念;尽管 pickle 读写文件对象,但它不处理命名持久化对象的问题,也不处理(更复杂)并发访问持久化对象的问题。pickle 模块可以将复杂对象转换为字节流,也可以将字节流转换为具有相同内部结构的对象。将这些字节流写入文件可能是最明显的做法,但也可以考虑通过网络发送它们或将其存储在数据库中。shelve 模块提供了一个简单的接口,用于在 DBM 风格的数据库文件中对对象进行 pickle 和 unpickle。

模块接口

要序列化对象层次结构,您只需调用 dumps() 函数。同样,要反序列化数据流,您只需调用 loads() 函数。但是,如果您希望对序列化和反序列化有更多控制,您可以分别创建 PicklerUnpickler 对象。

pickle 模块提供以下常量

pickle.HIGHEST_PROTOCOL

一个整数,表示可用的最高协议版本。此值可以作为 protocol 值传递给函数 dump()dumps() 以及 Pickler 构造函数。

pickle.DEFAULT_PROTOCOL

一个整数,表示 pickling 时使用的默认协议版本。可能小于 HIGHEST_PROTOCOL。目前默认协议是 5,在 Python 3.8 中引入,与以前的版本不兼容。此版本引入了对带外缓冲区的支持,其中 PEP 3118 兼容数据可以与主 pickle 流分离传输。

版本 3.0 中的变更: 默认协议为 3。

版本 3.8 中的变更: 默认协议为 4。

版本 3.14 中的变更: 默认协议为 5。

pickle 模块提供以下函数,使 pickling 过程更加方便

pickle.dump(obj, file, protocol=None, *, fix_imports=True, buffer_callback=None)

将对象 obj 的 pickled 表示写入打开的文件对象 file。这等价于 Pickler(file, protocol).dump(obj)

参数 fileprotocolfix_importsbuffer_callbackPickler 构造函数中的含义相同。

版本 3.8 中的变更: 添加了 buffer_callback 参数。

pickle.dumps(obj, protocol=None, *, fix_imports=True, buffer_callback=None)

返回对象 obj 的 pickled 表示作为 bytes 对象,而不是将其写入文件。

参数 protocolfix_importsbuffer_callbackPickler 构造函数中的含义相同。

版本 3.8 中的变更: 添加了 buffer_callback 参数。

pickle.load(file, *, fix_imports=True, encoding='ASCII', errors='strict', buffers=None)

从打开的文件对象 file 读取对象的 pickled 表示,并返回其中指定的重构对象层次结构。这等价于 Unpickler(file).load()

pickle 的协议版本会自动检测,因此无需协议参数。对象 pickled 表示之后的字节将被忽略。

参数 filefix_importsencodingerrorsstrictbuffersUnpickler 构造函数中的含义相同。

版本 3.8 中的变更: 添加了 buffers 参数。

pickle.loads(data, /, *, fix_imports=True, encoding='ASCII', errors='strict', buffers=None)

返回对象 pickled 表示 data 的重构对象层次结构。data 必须是 字节类对象

pickle 的协议版本会自动检测,因此无需协议参数。对象 pickled 表示之后的字节将被忽略。

参数 fix_importsencodingerrorsstrictbuffersUnpickler 构造函数中的含义相同。

版本 3.8 中的变更: 添加了 buffers 参数。

pickle 模块定义了三个异常

exception pickle.PickleError

其他 pickling 异常的通用基类。它继承自 Exception

exception pickle.PicklingError

Pickler 遇到不可 pickled 的对象时引发的错误。它继承自 PickleError

请参阅哪些对象可以 pickled 和 unpickled?以了解哪些类型的对象可以 pickled。

exception pickle.UnpicklingError

当反序列化对象出现问题时(例如数据损坏或安全漏洞)引发的错误。它继承自 PickleError

请注意,在反序列化期间也可能引发其他异常,包括(但不限于)AttributeError、EOFError、ImportError 和 IndexError。

pickle 模块导出三个类:PicklerUnpicklerPickleBuffer

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,pickle 将尝试将新的 Python 3 名称映射到 Python 2 中使用的旧模块名称,以便 Python 2 可以读取 pickle 数据流。

如果 buffer_callbackNone (默认值),则缓冲区视图将作为 pickle 流的一部分序列化到 file 中。

如果 buffer_callback 不为 None,则它可以被调用任意次,并带有一个缓冲区视图。如果回调函数返回一个假值(例如 None),则给定缓冲区是带外的;否则,缓冲区将带内序列化,即在 pickle 流内部序列化。

如果 buffer_callback 不为 NoneprotocolNone 或小于 5,则会引发错误。

版本 3.8 中的变更: 添加了 buffer_callback 参数。

dump(obj)

obj 的 pickled 表示写入构造函数中给定的打开文件对象。

persistent_id(obj)

默认情况下不执行任何操作。此方法旨在供子类覆盖。

如果 persistent_id() 返回 None,则 obj 将照常被 pickle。任何其他值都会导致 Pickler 将返回的值作为 obj 的持久 ID 发出。此持久 ID 的含义应由 Unpickler.persistent_load() 定义。请注意,persistent_id() 返回的值本身不能具有持久 ID。

有关详细信息和使用示例,请参阅外部对象的持久化

版本 3.13 中的变更: Pickler 的 C 实现中添加了此方法的默认实现。

dispatch_table

一个 pickler 对象的调度表是 缩减函数 的注册表,这种函数可以使用 copyreg.pickle() 声明。它是一个映射,其键是类,值是缩减函数。缩减函数接受关联类的单个参数,并且应符合与 __reduce__() 方法相同的接口。

默认情况下,pickler 对象不会有 dispatch_table 属性,而是会使用由 copyreg 模块管理的全局调度表。但是,为了自定义特定 pickler 对象的 pickling 行为,可以将 dispatch_table 属性设置为一个类似字典的对象。或者,如果 Pickler 的子类具有 dispatch_table 属性,则它将用作该类实例的默认调度表。

有关使用示例,请参阅调度表

在 3.3 版本加入。

reducer_override(obj)

可在 Pickler 子类中定义的特殊缩减器。此方法优先于 dispatch_table 中的任何缩减器。它应符合与 __reduce__() 方法相同的接口,并且可以选择返回 NotImplemented 以回退到 dispatch_table 注册的缩减器来 pickle obj

有关详细示例,请参阅类型、函数和其他对象的自定义缩减

在 3.8 版本加入。

fast

已弃用。如果设置为真值,则启用快速模式。快速模式禁用备忘录的使用,从而通过不生成多余的 PUT 操作码来加快 pickling 过程。它不应与自引用对象一起使用,否则会导致 Pickler 无限递归。

如果您需要更紧凑的 pickle,请使用 pickletools.optimize()

clear_memo()

清除 pickler 的“备忘录”。

备忘录是记录 pickler 已经见过的对象的数据结构,因此共享或递归对象是通过引用而不是值来 pickle 的。此方法在重复使用 pickler 时很有用。

class pickle.Unpickler(file, *, fix_imports=True, encoding='ASCII', errors='strict', buffers=None)

这接受一个二进制文件用于读取 pickle 数据流。

pickle 的协议版本会自动检测,因此不需要协议参数。

参数 file 必须有三个方法:一个接受整数参数的 read() 方法,一个接受缓冲区参数的 readinto() 方法,以及一个不需要参数的 readline() 方法,与 io.BufferedIOBase 接口一致。因此,file 可以是为二进制读取而打开的磁盘文件、io.BytesIO 对象,或任何满足此接口的其他自定义对象。

可选参数 fix_importsencodingerrors 用于控制对 Python 2 生成的 pickle 流的兼容性支持。如果 fix_imports 为 True,pickle 将尝试将旧的 Python 2 名称映射到 Python 3 中使用的新名称。encodingerrors 告诉 pickle 如何解码 Python 2 pickled 的 8 位字符串实例;它们默认分别为 ‘ASCII’ 和 ‘strict’。encoding 可以是 ‘bytes’ 以将这些 8 位字符串实例读取为 bytes 对象。对于解 pickle NumPy 数组和 Python 2 pickled 的 datetimedatetime 实例,需要使用 encoding='latin1'

如果 buffersNone (默认值),则反序列化所需的所有数据必须包含在 pickle 流中。这意味着当实例化 Pickler 时 (或当调用 dump()dumps() 时),buffer_callback 参数为 None

如果 buffers 不为 None,它应该是一个可迭代的、支持缓冲区的对象序列,每次 pickle 流引用带外缓冲区视图时都会被消耗。这些缓冲区已按顺序提供给 Pickler 对象的 buffer_callback

版本 3.8 中的变更: 添加了 buffers 参数。

load()

从构造函数中给定的打开文件对象读取对象的 pickled 表示,并返回其中指定的重构对象层次结构。对象 pickled 表示之后的字节将被忽略。

persistent_load(pid)

默认情况下引发 UnpicklingError

如果定义了,persistent_load() 应该返回由持久 ID pid 指定的对象。如果遇到无效的持久 ID,则应引发 UnpicklingError

有关详细信息和使用示例,请参阅外部对象的持久化

版本 3.13 中的变更: Unpickler 的 C 实现中添加了此方法的默认实现。

find_class(module, name)

如果需要,导入 module 并从中返回名为 name 的对象,其中 modulename 参数是 str 对象。请注意,与其名称暗示的不同,find_class() 也用于查找函数。

子类可以覆盖此方法,以控制加载何种类型的对象以及如何加载它们,从而可能降低安全风险。有关详细信息,请参阅限制全局变量

引发一个审计事件 pickle.find_class,参数为 module, name

class pickle.PickleBuffer(buffer)

一个表示可 pickle 数据的缓冲区的包装器。buffer 必须是一个提供缓冲区的对象,例如字节类对象或 N 维数组。

PickleBuffer 对象本身就是一个缓冲区提供者,因此可以将其传递给其他期望提供缓冲区的 API,例如 memoryview

PickleBuffer 对象只能使用 pickle 协议 5 或更高版本进行序列化。它们适用于带外序列化

在 3.8 版本加入。

raw()

返回此缓冲区底层内存区域的 memoryview。返回的对象是一个一维、C 连续的 memoryview,格式为 B (无符号字节)。如果缓冲区既不是 C 连续也不是 Fortran 连续,则会引发 BufferError

release()

释放 PickleBuffer 对象暴露的底层缓冲区。

哪些对象可以 pickled 和 unpickled?

以下类型可以 pickled

  • 内置常量(NoneTrueFalseEllipsisNotImplemented);

  • 整数、浮点数、复数;

  • 字符串、字节、字节数组;

  • 只包含可 pickle 对象的元组、列表、集合和字典;

  • 可从模块顶层访问的函数(内置和用户定义)(使用 def,而不是 lambda);

  • 可从模块顶层访问的类;

  • 这些类的实例,其调用 __getstate__() 的结果是可 pickle 的(详见pickling 类实例部分)。

尝试 pickle 不可 pickle 的对象将引发 PicklingError 异常;发生这种情况时,可能已经有未指定数量的字节写入到底层文件。尝试 pickle 高度递归的数据结构可能会超出最大递归深度,在这种情况下将引发 RecursionError。您可以谨慎地使用 sys.setrecursionlimit() 提高此限制。

请注意,函数(内置和用户定义的)是通过完全限定名称进行 pickle 的,而不是通过值。[2] 这意味着只 pickle 函数名称,以及包含模块和类的名称。函数的代码或其任何函数属性都不会被 pickle。因此,定义模块必须在 unpickling 环境中可导入,并且模块必须包含命名的对象,否则将引发异常。[3]

同样,类是通过完全限定名称进行 pickle 的,因此 unpickling 环境中的限制也适用。请注意,类的代码或数据都不会被 pickle,因此在以下示例中,类属性 attr 在 unpickling 环境中不会被恢复

class Foo:
    attr = 'A class attribute'

picklestring = pickle.dumps(Foo)

这些限制是可 pickle 的函数和类必须在模块顶层定义的原因。

同样,当类实例被 pickle 时,它们的类代码和数据不会随它们一起被 pickle。只有实例数据被 pickle。这是有意为之的,因此您可以修复类中的错误或向类添加方法,并且仍然可以加载使用早期版本类创建的对象。如果您计划拥有长生命周期的对象,这些对象将经历多个版本的类,那么在对象中放置版本号可能是有意义的,以便类的 __setstate__() 方法可以进行适当的转换。

Pickling 类实例

在本节中,我们将介绍可用于定义、定制和控制类实例如何被 pickle 和 unpickle 的通用机制。

在大多数情况下,无需额外的代码即可使实例可 pickle。默认情况下,pickle 将通过自省检索类的属性和实例的属性。当类实例被 unpickle 时,其 __init__() 方法通常会被调用。默认行为首先创建一个未初始化的实例,然后恢复保存的属性。以下代码显示了此行为的实现

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 及更高版本中,实现了 __getnewargs_ex__() 方法的类可以指定在 unpickling 时传递给 __new__() 方法的值。该方法必须返回一对 (args, kwargs),其中 args 是位置参数的元组,kwargs 是用于构造对象的命名参数的字典。这些参数将在 unpickling 时传递给 __new__() 方法。

如果您的类的 __new__() 方法需要仅限关键字的参数,您应该实现此方法。否则,为了兼容性,建议实现 __getnewargs__()

版本 3.6 中的变更: __getnewargs_ex__() 现在在协议 2 和 3 中使用。

object.__getnewargs__()

此方法的作用与 __getnewargs_ex__() 类似,但仅支持位置参数。它必须返回一个参数元组 args,该元组将在 unpickling 时传递给 __new__() 方法。

如果定义了 __getnewargs_ex__(),则不会调用 __getnewargs__()

版本 3.6 中的变更: 在 Python 3.6 之前,在协议 2 和 3 中调用的是 __getnewargs__() 而不是 __getnewargs_ex__()

object.__getstate__()

类可以通过重写方法 __getstate__() 进一步影响其实例如何被 pickle。它会被调用,并且返回的对象将作为实例的内容被 pickle,而不是默认状态。有几种情况

  • 对于没有实例 __dict__ 也没有 __slots__ 的类,默认状态为 None

  • 对于具有实例 __dict__ 但没有 __slots__ 的类,默认状态是 self.__dict__

  • 对于同时具有实例 __dict____slots__ 的类,默认状态是一个由两个字典组成的元组:self.__dict__ 和一个将槽名映射到槽值的字典。后者只包含有值的槽。

  • 对于具有 __slots__ 而没有实例 __dict__ 的类,默认状态是一个元组,其第一个项是 None,第二个项是上一点中描述的将槽名映射到槽值的字典。

版本 3.11 中的变更: object 类中添加了 __getstate__() 方法的默认实现。

object.__setstate__(state)

在 unpickling 时,如果类定义了 __setstate__(),则会使用 unpickled 状态调用它。在这种情况下,状态对象没有必要是字典。否则,pickled 状态必须是字典,并且其项将分配给新实例的字典。

备注

如果 __reduce__() 在 pickling 时返回状态值为 None,则在 unpickling 时不会调用 __setstate__() 方法。

有关如何使用方法 __getstate__()__setstate__() 的更多信息,请参阅处理有状态对象部分。

备注

在 unpickling 时,实例上可能会调用一些方法,例如 __getattr__()__getattribute__()__setattr__()。如果这些方法依赖于某个内部不变量为真,则类型应实现 __new__() 来建立此不变量,因为在 unpickling 实例时不会调用 __init__()

正如我们将看到的,pickle 不会直接使用上述方法。实际上,这些方法是复制协议的一部分,该协议实现了 __reduce__() 特殊方法。复制协议为检索 pickle 和复制对象所需的数据提供了统一的接口。[4]

尽管功能强大,但直接在类中实现 __reduce__() 容易出错。因此,类设计者应尽可能使用高级接口(即 __getnewargs_ex__()__getstate__()__setstate__())。然而,我们将展示一些情况下,使用 __reduce__() 是唯一的选择或导致更高效的 pickling,或两者兼而有之。

object.__reduce__()

接口目前定义如下。 __reduce__() 方法不接受任何参数,并且应返回字符串或最好是元组(返回的对象通常称为“缩减值”)。

如果返回一个字符串,则该字符串应被解释为全局变量的名称。它应该是对象相对于其模块的本地名称;pickle 模块会在模块命名空间中搜索以确定对象的模块。此行为通常对于单例很有用。

当返回一个元组时,它的长度必须在两到六项之间。可选项可以省略,也可以提供 None 作为其值。每个项的语义按顺序如下

  • 将要调用以创建对象初始版本的可调用对象。

  • 可调用对象的参数元组。如果可调用对象不接受任何参数,则必须提供一个空元组。

  • 可选地,对象的内部状态,该状态将如前所述传递给对象的 __setstate__() 方法。如果对象没有这样的方法,那么该值必须是字典,并且它将被添加到对象的 __dict__ 属性中。

  • 可选地,一个迭代器(而不是序列)生成连续的项。这些项将使用 obj.append(item) 或批量使用 obj.extend(list_of_items) 附加到对象。这主要用于列表子类,但只要它们具有具有适当签名的 append()extend() 方法,也可以用于其他类。(使用 append() 还是 extend() 取决于使用的 pickle 协议版本以及要附加的项数,因此两者都必须支持。)

  • 可选地,一个迭代器(不是序列)生成连续的键值对。这些项将使用 obj[key] = value 存储到对象中。这主要用于字典子类,但只要它们实现了 __setitem__(),也可以用于其他类。

  • 可选地,一个带有 (obj, state) 签名的可调用对象。此可调用对象允许用户以编程方式控制特定对象的状态更新行为,而不是使用 obj 的静态 __setstate__() 方法。如果不是 None,此可调用对象将优先于 obj__setstate__()

    版本 3.8 新增: 添加了可选的第六个元组项,(obj, state)

object.__reduce_ex__(protocol)

或者,可以定义一个 __reduce_ex__() 方法。唯一的区别是此方法应接受一个整数参数,即协议版本。如果定义了此方法,pickle 将优先于 __reduce__() 方法。此外,__reduce__() 自动成为扩展版本的同义词。此方法的主要用途是为旧版 Python 版本提供向后兼容的缩减值。

外部对象的持久化

为了实现对象的持久化,pickle 模块支持对序列化数据流之外的对象进行引用。这些对象通过持久 ID 进行引用,持久 ID 对于协议 0 应该是字母数字字符串[5],对于任何更新的协议可以是任意对象。

此类持久 ID 的解析不由 pickle 模块定义;它会将此解析委托给序列化器和反序列化器中用户定义的方法,即 persistent_id()persistent_load()

要序列化具有外部持久 ID 的对象,序列化器必须具有自定义的 persistent_id() 方法,该方法将对象作为参数,并返回 None 或该对象的持久 ID。当返回 None 时,序列化器会像往常一样序列化对象。当返回持久 ID 字符串时,序列化器将序列化该对象,并带有一个标记,以便反序列化器将其识别为持久 ID。

要反序列化外部对象,反序列化器必须具有自定义的 persistent_load() 方法,该方法接受持久 ID 对象并返回引用的对象。

这是一个综合示例,展示了如何使用持久 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()

调度表

如果想要自定义某些类的序列化,而不干扰任何其他依赖序列化的代码,则可以创建一个带有私有调度表的序列化器。

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:
    """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!'

类型、函数和其他对象的自定义简化

在 3.8 版本加入。

有时,dispatch_table 可能不够灵活。特别是我们可能希望根据对象类型以外的其他条件自定义序列化,或者我们可能希望自定义函数和类的序列化。

对于这些情况,可以从 Pickler 类继承并实现 reducer_override() 方法。此方法可以返回任意简化元组(请参阅 __reduce__())。它也可以返回 NotImplemented 以回退到传统行为。

如果 dispatch_tablereducer_override() 都已定义,则 reducer_override() 方法优先。

备注

出于性能原因,reducer_override() 可能不会为以下对象调用:NoneTrueFalse,以及 intfloatbytesstrdictsetfrozensetlisttuple 的精确实例。

这里有一个简单的例子,我们允许序列化和重建给定的类

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

带外缓冲区

在 3.8 版本加入。

在某些情况下,pickle 模块用于传输大量数据。因此,最小化内存复制次数以保持性能和资源消耗非常重要。然而,pickle 模块的正常操作,因为它将对象的图结构转换为字节的顺序流,本质上涉及将数据复制到序列化流和从中复制数据。

如果 提供者 (要传输的对象类型的实现)和 消费者 (通信系统的实现)都支持 pickle 协议 5 及更高版本提供的带外传输功能,则可以避免此限制。

提供者 API

要序列化的大数据对象必须实现专门用于协议 5 及更高版本的 __reduce_ex__() 方法,该方法为任何大数据返回一个 PickleBuffer 实例(而不是例如 bytes 对象)。

一个 PickleBuffer 对象 表示 底层缓冲区符合带外数据传输的条件。这些对象与 pickle 模块的正常使用兼容。但是,消费者也可以选择告知 pickle 他们将自行处理这些缓冲区。

消费者 API

通信系统可以启用对序列化对象图时生成的 PickleBuffer 对象的自定义处理。

在发送方,它需要将 buffer_callback 参数传递给 Pickler (或传递给 dump()dumps() 函数),该参数将在序列化对象图时生成的每个 PickleBuffer 中调用。由 buffer_callback 累积的缓冲区将不会将其数据复制到 pickle 流中,只会插入一个廉价的标记。

在接收方,它需要将 buffers 参数传递给 Unpickler (或传递给 load()loads() 函数),这是一个 buffers 可迭代对象,其中包含传递给 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 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)

重构器(_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: a copy was made

但是,如果我们传递一个 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: no copy was made

此示例受 bytearray 分配自身内存的事实限制:你无法创建由另一个对象的内存支持的 bytearray 实例。然而,第三方数据类型(如 NumPy 数组)没有此限制,并允许在不同进程或系统之间传输时使用零拷贝序列化(或尽可能少的拷贝)。

参见

PEP 574 – 带外数据的 Pickle 协议 5

限制全局变量

默认情况下,反序列化将导入它在序列化数据中找到的任何类或函数。对于许多应用程序,此行为是不可接受的,因为它允许反序列化器导入和调用任意代码。只需考虑这个手工制作的序列化数据流在加载时会做什么

>>> import pickle
>>> pickle.loads(b"cos\nsystem\n(S'echo hello world'\ntR.")
hello world
0

在此示例中,反序列化器导入 os.system() 函数,然后应用字符串参数“echo hello world”。尽管此示例无害,但很容易想象一个可能损坏您系统的示例。

因此,您可能希望通过自定义 Unpickler.find_class() 来控制哪些内容可以被反序列化。与其名称所暗示的不同,每当请求全局变量(即类或函数)时都会调用 Unpickler.find_class()。因此,可以完全禁止全局变量或将它们限制在安全子集中。

这是一个反序列化器示例,它只允许从 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()

我们的反序列化器按预期工作的示例用法

>>> 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 协议版本(从协议 2 及以上)为几种常见功能和内置类型提供了高效的二进制编码。此外,pickle 模块有一个用 C 编写的透明优化器。

示例

对于最简单的代码,请使用 dump()load() 函数。

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)

以下示例读取生成的序列化数据。

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)

命令行界面

pickle 模块可以作为命令行脚本调用,它将显示 pickle 文件的内容。但是,当您要检查的 pickle 文件来自不受信任的源时,-m pickletools 是一个更安全的选择,因为它不执行 pickle 字节码,请参阅 pickletools CLI 使用

python -m pickle pickle_file [pickle_file ...]

接受以下选项

pickle_file

要读取的 pickle 文件,或 - 表示从标准输入读取。

参见

模块 copyreg

用于扩展类型的 Pickle 接口构造函数注册。

模块 pickletools

用于处理和分析序列化数据的工具。

模块 shelve

索引对象数据库;使用 pickle

模块 copy

浅层和深层对象复制。

模块 marshal

内置类型的高性能序列化。

脚注