pickle — Python 对象序列化

源代码: Lib/pickle.py


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

警告

pickle 模块不安全。只 unpickle 你信任的数据。

可以构造恶意的 pickle 数据,这些数据在 unpickling 期间会执行任意代码。永远不要 unpickle 可能来自不可信来源或可能被篡改的数据。

如果你需要确保数据没有被篡改,请考虑使用 hmac 签署数据。

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

与其他 Python 模块的关系

marshal 的比较

Python 有一个更原始的序列化模块,称为 marshal,但通常 pickle 应该是序列化 Python 对象的首选方式。marshal 的存在主要是为了支持 Python 的 .pyc 文件。

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

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

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

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

  • marshal 序列化格式不能保证在 Python 版本之间可移植。由于它的主要工作是支持 .pyc 文件,因此 Python 实现者保留在必要时以不向后兼容的方式更改序列化格式的权利。如果选择了兼容的 pickle 协议,并且 pickling 和 unpickling 代码处理 Python 2 到 Python 3 的类型差异(如果你的数据跨越了这种独特的突破性语言边界),则 pickle 序列化格式保证在 Python 版本之间向后兼容。

json 的比较

pickle 协议和 JSON(JavaScript 对象表示法)之间存在根本差异

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

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

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

  • 默认情况下,JSON 只能表示 Python 内置类型的一个子集,而不能表示任何自定义类;pickle 可以表示数量非常庞大的 Python 类型(其中许多类型通过巧妙地使用 Python 的自省工具自动表示;复杂的案例可以通过实现特定的对象 API来解决);

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

另请参阅

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

数据流格式

pickle 使用的数据格式是 Python 特有的。这样做的好处是不受外部标准(如 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 中添加。它增加了对超大对象的支持、可以序列化更多类型的对象以及一些数据格式优化。它是从 Python 3.8 开始的默认协议。有关协议 4 带来的改进,请参阅 PEP 3154

  • 协议版本 5 在 Python 3.8 中添加。它增加了对带外数据的支持以及对带内数据的加速。有关协议 5 带来的改进,请参阅 PEP 574

注意

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

模块接口

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

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)

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

参数 fileprotocolfix_importsbuffer_callback 的含义与 Pickler 构造函数中的含义相同。

在 3.8 版本中更改: 添加了 buffer_callback 参数。

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

bytes 对象的形式返回对象 obj 的序列化表示,而不是将其写入文件。

参数 protocolfix_importsbuffer_callback 的含义与 Pickler 构造函数中的含义相同。

在 3.8 版本中更改: 添加了 buffer_callback 参数。

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

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

会自动检测 pickle 的协议版本,因此不需要协议参数。会忽略对象序列化表示之后的字节。

参数 filefix_importsencodingerrorsstrictbuffers 的含义与 Unpickler 构造函数中的含义相同。

在 3.8 版本中更改: 添加了 buffers 参数。

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

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

会自动检测 pickle 的协议版本,因此不需要协议参数。会忽略对象序列化表示之后的字节。

参数 fix_importsencodingerrorsstrictbuffers 的含义与 Unpickler 构造函数中的含义相同。

在 3.8 版本中更改: 添加了 buffers 参数。

pickle 模块定义了三个异常

exception pickle.PickleError

其他序列化异常的通用基类。它继承自 Exception

exception pickle.PicklingError

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

请参阅 哪些可以被序列化和反序列化? 以了解哪些类型的对象可以被序列化。

exception pickle.UnpicklingError

当解封(unpickling)对象时出现问题时引发的错误,例如数据损坏或安全违规。它继承自 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 中使用的旧模块名称,以便 pickle 数据流可被 Python 2 读取。

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

如果 buffer_callback 不为 None,则可以使用缓冲区视图多次调用它。如果回调返回 false 值(例如 None),则给定的缓冲区是带外的;否则,缓冲区将带内序列化,即在 pickle 流内部。

如果 buffer_callback 不为 NoneprotocolNone 或小于 5,则会出错。

在 3.8 版本中更改: 添加了 buffer_callback 参数。

dump(obj)

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

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 属性设置为类似 dict 的对象。或者,如果 Pickler 的子类具有 dispatch_table 属性,则它将用作该类实例的默认调度表。

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

在 3.3 版本中添加。

reducer_override(obj)

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

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

在 3.8 版本中添加。

fast

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

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

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 pickling 的 8 位字符串实例;这些参数分别默认为“ASCII”和“strict”。encoding 可以为“bytes”,将这些 8 位字符串实例读取为字节对象。对于解封 NumPy 数组和 datetimedatetime (由 Python 2 pickling) 的实例,需要使用 encoding='latin1'

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

如果 buffers 不为 None,它应该是一个可迭代的、启用缓冲区的对象,每次 pickle 数据流引用带外缓冲区视图时都会使用它。这些缓冲区已按照 Pickler 对象的 buffer_callback 的顺序给出。

在 3.8 版本中更改: 添加了 buffers 参数。

load()

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

persistent_load(pid)

默认情况下引发 UnpicklingError 异常。

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

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

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

find_class(module, name)

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

子类可以重写此方法以控制加载的对象类型以及加载方式,从而潜在地降低安全风险。有关详细信息,请参阅限制全局变量

使用参数 modulename 引发审计事件 pickle.find_class

class pickle.PickleBuffer(buffer)

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

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

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

在 3.8 版本中添加。

raw()

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

release()

释放 PickleBuffer 对象公开的底层缓冲区。

哪些对象可以被 pickle 和 unpickle?

以下类型可以被 pickle

  • 内置常量 (None, True, False, Ellipsis, 和 NotImplemented);

  • 整数、浮点数、复数;

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

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

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

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

  • 此类类的实例,其调用 __getstate__() 的结果是可 pickle 的(有关详细信息,请参阅 Pickling 类实例 部分)。

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

请注意,函数(内置函数和用户定义函数)是通过完全限定名称而不是值来 pickle 的。[2] 这意味着只 pickle 函数名称,以及包含模块和类的名称。既不 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 版本中更改: 现在在协议 2 和 3 中使用 __getnewargs_ex__()

object.__getnewargs__()

此方法与 __getnewargs_ex__() 的作用类似,但仅支持位置参数。它必须返回一个参数元组 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__ 的类,默认状态是由两个字典组成的元组:self.__dict__ 和一个将槽名称映射到槽值的字典。只有具有值的槽才会包含在后者中。

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

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

object.__setstate__(state)

在反序列化时,如果类定义了 __setstate__(),则会使用反序列化的状态调用它。在这种情况下,状态对象不需要是字典。否则,序列化的状态必须是字典,并且其条目将分配给新实例的字典。

注意

如果 __reduce__() 在序列化时返回一个值为 None 的状态,则在反序列化时不会调用 __setstate__() 方法。

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

注意

在反序列化时,可能会在实例上调用诸如 __getattr__()__getattribute__()__setattr__() 之类的方法。如果这些方法依赖于某些内部不变量为真,则类型应实现 __new__() 以建立这样的不变量,因为在反序列化实例时不会调用 __init__()

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

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

object.__reduce__()

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

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

当返回元组时,它必须包含两个到六个条目。可选条目可以省略,也可以提供 None 作为其值。每个条目的语义依次为:

  • 一个可调用对象,将调用该对象以创建对象的初始版本。

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

  • 可选地,对象的 state,它将传递给对象的 __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 版本提供向后兼容的 reduce 值。

外部对象的持久化

为了方便对象的持久化,pickle 模块支持引用 pickle 数据流之外的对象的概念。此类对象通过持久 ID 引用,该 ID 应该是字母数字字符的字符串(对于协议 0) [5] 或只是一个任意对象(对于任何较新的协议)。

此类持久 ID 的解析不是由 pickle 模块定义的;它会将此解析委托给 pickler 和 unpickler 上的用户定义方法,分别为 persistent_id()persistent_load()

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

要 unpickle 外部对象,unpickler 必须具有一个自定义的 persistent_load() 方法,该方法接受一个持久 ID 对象并返回被引用的对象。

这是一个全面的示例,展示了如何使用持久 ID 通过引用来 pickle 外部对象。

# 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()

分发表

如果希望自定义某些类的 pickling,而不影响任何其他依赖 pickling 的代码,则可以创建一个具有私有分发表的 pickler。

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 模块的所有用户共享的全局分发表。

处理有状态对象

这是一个示例,展示了如何修改类的 pickling 行为。下面的 TextReader 类打开一个文本文件,并在每次调用其 readline() 方法时返回行号和行内容。如果 pickle 一个 TextReader 实例,则保存 除了 文件对象成员之外的所有属性。当 unpickle 该实例时,文件将重新打开,并且从上次位置恢复读取。__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 可能不够灵活。特别是,我们可能希望基于对象类型以外的其他标准自定义 pickling,或者我们可能希望自定义函数和类的 pickling。

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

如果同时定义了 dispatch_tablereducer_override(),则 reducer_override() 方法优先。

注意

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

这是一个简单的示例,我们允许 pickle 和重建给定的类

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 流和从中复制数据。

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

提供者 API

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

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

使用者 API

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

在发送端,需要将 buffer_callback 参数传递给 Pickler (或传递给 dump()dumps() 函数),当对对象图进行 pickle 操作时,生成的每个 PickleBuffer 都将调用此参数。buffer_callback 累积的缓冲区的数据不会被复制到 pickle 流中,只会插入一个廉价的标记。

在接收端,需要将 buffers 参数传递给 Unpickler (或传递给 load()loads() 函数),该参数是传递给 buffer_callback 的缓冲区的可迭代对象。此可迭代对象应按照传递给 buffer_callback 的顺序生成缓冲区。这些缓冲区将提供由对象的重构器预期的数据,这些对象的 pickle 操作生成了原始的 PickleBuffer 对象。

在发送端和接收端之间,通信系统可以自由地实现其自己的带外缓冲区传输机制。潜在的优化包括使用共享内存或依赖于数据类型的压缩。

示例

这是一个简单的示例,其中我们实现了一个 bytearray 子类,该子类能够参与带外缓冲区 pickle 操作

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 类方法)如果缓冲区具有正确的类型,则返回缓冲区提供的对象。这是在此玩具示例中模拟零拷贝行为的一种简单方法。

在使用者端,我们可以像往常一样 pickle 这些对象,在反序列化后,我们会得到原始对象的副本

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 数组之类的第三方数据类型没有此限制,并且在不同的进程或系统之间传输时,允许使用零拷贝 pickle 操作(或尽可能少地复制)。

另请参阅

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

限制全局变量

默认情况下,unpickle 操作会导入它在 pickle 数据中找到的任何类或函数。对于许多应用程序,此行为是不可接受的,因为它允许 unpickler 导入和调用任意代码。请考虑加载此手工制作的 pickle 数据流时会发生什么

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

在此示例中,unpickler 导入 os.system() 函数,然后应用字符串参数 “echo hello world”。尽管此示例是无害的,但不难想象一个可能会损害您的系统的示例。

因此,您可能需要通过自定义 Unpickler.find_class() 来控制 unpickle 的内容。与名称所暗示的不同,每当请求全局变量(即类或函数)时,都会调用 Unpickler.find_class()。因此,可以完全禁止全局变量或将它们限制为安全的子集。

这是 unpickler 的一个示例,该 unpickler 仅允许加载来自 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()

我们 unpickler 的示例用法按预期工作

>>> 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

正如我们的示例所示,您必须小心允许 unpickle 的内容。因此,如果安全是一个问题,您可能需要考虑其他替代方案,例如 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)

以下示例读取生成的 pickle 数据。

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)

另请参阅

模块 copyreg

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

模块 pickletools

用于处理和分析 pickle 数据的工具。

模块 shelve

对象的索引数据库;使用 pickle

模块 copy

浅层和深层对象复制。

模块 marshal

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

脚注