pickle — Python 对象序列化 — Python 文档
pickle — Python 对象序列化
pickle 模块实现了用于序列化和反序列化 Python 对象结构的二进制协议。 “Pickling” 是将 Python 对象层次结构转换为字节流的过程,而 “unpickling” 是逆操作,由此字节流(来自 二进制file 或 bytes-like object) 被转换回对象层次结构。 酸洗(和取消酸洗)也称为“序列化”、“编组”、1 或“扁平化”; 然而,为了避免混淆,这里使用的术语是“酸洗”和“解酸”。
警告
pickle
模块 不安全 。 只解压您信任的数据。
有可能构建恶意pickle数据,在unpickling期间执行任意代码。 永远不要解开可能来自不受信任的来源或可能被篡改的数据。
如果您需要确保数据未被篡改,请考虑使用 hmac 对数据进行签名。
如果您正在处理不受信任的数据,更安全的序列化格式(例如 json)可能更合适。 参见 与 json 的比较。
与其他 Python 模块的关系
与marshal的比较
Python 有一个更原始的序列化模块,称为 marshal,但总的来说,pickle 应该始终是序列化 Python 对象的首选方式。 marshal 的存在主要是为了支持 Python 的 .pyc
文件。
pickle 模块跟踪它已经序列化的对象,以便以后对同一对象的引用不会再次序列化。 marshal 不这样做。
这对递归对象和对象共享都有影响。 递归对象是包含对自身的引用的对象。 这些不是由 marshal 处理的,实际上,尝试 marshal 递归对象会使您的 Python 解释器崩溃。 当在被序列化的对象层次结构中的不同位置对同一对象有多个引用时,就会发生对象共享。 pickle 仅存储此类对象一次,并确保所有其他引用都指向主副本。 共享对象保持共享状态,这对于可变对象非常重要。
marshal 不能用于序列化用户定义的类及其实例。 pickle 可以透明地保存和恢复类实例,但是类定义必须是可导入的,并且与存储对象时存在于同一模块中。
marshal 序列化格式不能保证跨 Python 版本可移植。 因为 Python 的主要工作是支持
.pyc
文件,所以 Python 实现者保留在需要时以非向后兼容的方式更改序列化格式的权利。 pickle 序列化格式保证在 Python 版本之间向后兼容,前提是选择了兼容的 pickle 协议,并且如果您的数据跨越了那个独特的突破性更改语言,pickle 和 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 本身不会产生任意代码执行漏洞。
数据流格式
pickle 使用的数据格式是 Python 特定的。 这样做的好处是不受外部标准的限制,例如 JSON 或 XDR(不能代表指针共享); 然而,这意味着非 Python 程序可能无法重建腌制的 Python 对象。
默认情况下,pickle 数据格式使用相对紧凑的二进制表示。 如果您需要最佳尺寸特征,您可以有效地 压缩 腌制数据。
模块 pickletools 包含用于分析由 pickle 生成的数据流的工具。 pickletools 源代码对pickle 协议使用的操作码有大量注释。
目前有 6 种不同的协议可用于酸洗。 使用的协议越高,读取生成的泡菜所需的 Python 版本就越新。
- 协议版本 0 是原始的“人类可读”协议,向后兼容早期版本的 Python。
- 协议版本 1 是一种旧的二进制格式,它也与早期版本的 Python 兼容。
- 协议版本 2 是在 Python 2.3 中引入的。 它为 新式类 提供了更有效的酸洗。 有关协议 2 带来的改进的信息,请参阅 PEP 307。
- Python 3.0 中添加了协议版本 3。 它明确支持 bytes 对象,并且不能被 Python 2.x 解压。 这是 Python 3.0-3.7 中的默认协议。
- Python 3.4 中添加了协议版本 4。 它增加了对超大对象的支持,酸洗更多种类的对象,以及一些数据格式优化。 它是从 Python 3.8 开始的默认协议。 有关协议 4 带来的改进的信息,请参阅 PEP 3154。
- Python 3.8 中添加了协议版本 5。 它增加了对带外数据的支持和对带内数据的加速。 有关协议 5 带来的改进的信息,请参阅 PEP 574。
模块接口
要序列化对象层次结构,只需调用 dumps() 函数。 类似地,要反序列化数据流,您可以调用 loads() 函数。 但是,如果您想对序列化和反序列化进行更多控制,则可以分别创建 Pickler 或 Unpickler 对象。
pickle 模块提供以下常量:
- pickle.DEFAULT_PROTOCOL
一个整数,默认 协议版本 用于酸洗。 可能小于 HIGHEST_PROTOCOL。 目前默认的协议是 4,最初是在 Python 3.4 中引入的,与之前的版本不兼容。
3.0版本变化:默认协议为3。
3.8版本变化:默认协议为4。
pickle模块提供了以下功能,让pickle过程更加方便:
- pickle.dump(obj, file, protocol=None, *, fix_imports=True, buffer_callback=None)
将对象 obj 的腌制表示写入打开的 文件对象 文件 。 这相当于
Pickler(file, protocol).dump(obj)
。参数 file、protocol、fix_imports 和 buffer_callback 与 Pickler 构造函数中的含义相同。
3.8 版更改: 添加了 buffer_callback 参数。
- pickle.dumps(obj, protocol=None, *, fix_imports=True, buffer_callback=None)
将对象 obj 的腌制表示返回为 bytes 对象,而不是将其写入文件。
参数 protocol、fix_imports 和 buffer_callback 与 Picker 构造函数中的含义相同。
3.8 版更改: 添加了 buffer_callback 参数。
- pickle.load(file, *, fix_imports=True, encoding='ASCII', errors='strict', buffers=None)
从打开的 文件对象 文件 中读取对象的酸洗表示,并返回其中指定的重组对象层次结构。 这相当于
Unpickler(file).load()
。pickle 的协议版本是自动检测的,因此不需要协议参数。 超过对象的pickle 表示的字节将被忽略。
参数 file、fix_imports、encoding、errors、strict 和 buffers与 Unpickler 构造函数中的含义相同。
3.8 版更改: 添加了 buffers 参数。
- pickle.loads(data, *, fix_imports=True, encoding='ASCII', errors='strict', buffers=None)
返回对象的pickle 表示data 的重构对象层次结构。 data 必须是一个 bytes-like object。
pickle 的协议版本是自动检测的,因此不需要协议参数。 超过对象的pickle 表示的字节将被忽略。
参数 file、fix_imports、encoding、errors、strict 和 buffers与 Unpickler 构造函数中的含义相同。
3.8 版更改: 添加了 buffers 参数。
pickle 模块定义了三个异常:
- exception pickle.PickleError
- 其他酸洗异常的公共基类。 它继承了 异常 。
- exception pickle.PicklingError
Pickler 遇到不可拾取的对象时引发错误。 它继承了 PickleError。
请参阅可以腌制和不腌制的物品?了解可以腌制的物品种类。
- exception pickle.UnpicklingError
当解压对象出现问题时引发错误,例如数据损坏或安全违规。 它继承了 PickleError。
请注意,在 unpickling 期间也可能引发其他异常,包括(但不一定限于)AttributeError、EOFError、ImportError 和 IndexError。
pickle 模块导出三个类,Pickle、Unpickler 和 PickleBuffer:
- 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_callback 为 None(默认值),则缓冲区视图将序列化为 file 作为 pickle 流的一部分。
如果 buffer_callback 不是 None,则可以使用缓冲区视图调用任意次数。 如果回调返回 false 值(例如 None),则给定的缓冲区为 带外 ; 否则缓冲区在带内序列化,即 泡菜流里面。
如果 buffer_callback 不是 None 并且 protocol 是 None 或小于 5,则会出错。
3.8 版更改: 添加了 buffer_callback 参数。
- dump(obj)
将 obj 的腌制表示写入构造函数中给出的打开文件对象。
- persistent_id(obj)
默认什么都不做。 它存在以便子类可以覆盖它。
如果 persistent_id() 返回
None
,obj 像往常一样被腌制。 任何其他值都会导致 Pickler 将返回值作为 obj 的持久 ID 发出。 这个持久ID的含义应该由Unpickler.persistent_load()定义。 请注意, persistent_id() 返回的值本身不能具有持久 ID。有关详细信息和使用示例,请参阅 外部对象持久性 。
- dispatch_table
Pickler 对象的调度表是 归约函数 的注册表,可以使用 copyreg.pickle() 声明。 它是一个映射,其键是类,值是归约函数。 归约函数采用关联类的单个参数,并且应符合与
__reduce__()
方法相同的接口。默认情况下,pickler 对象将没有 dispatch_table 属性,而是使用由 copyreg 模块管理的全局调度表。 但是,要为特定的pickler 对象自定义酸洗,可以将 dispatch_table 属性设置为类似 dict 的对象。 或者,如果 Pickler 的子类具有 dispatch_table 属性,那么这将用作该类实例的默认调度表。
有关用法示例,请参阅 调度表 。
3.3 版中的新功能。
- reducer_override(self, obj)
可以在 Picker 子类中定义的特殊减速器。 此方法优先于 dispatch_table 中的任何减速器。 它应该符合与
__reduce__()
方法相同的接口,并且可以选择返回NotImplemented
以回退 dispatch_table 注册的减速器以腌制obj
。有关详细示例,请参阅 类型、函数和其他对象的自定义缩减 。
3.8 版中的新功能。
- fast
已弃用。 如果设置为真值,则启用快速模式。 快速模式禁用备忘录的使用,因此通过不生成多余的 PUT 操作码来加速酸洗过程。 它不应该与自引用对象一起使用,否则会导致 Picker 无限递归。
如果您需要更紧凑的泡菜,请使用 pickletools.optimize()。
- class pickle.Unpickler(file, *, fix_imports=True, encoding='ASCII', errors='strict', buffers=None)
这需要一个二进制文件来读取泡菜数据流。
pickle 的协议版本是自动检测的,因此不需要协议参数。
参数 file 必须有三个方法,一个采用整数参数的 read() 方法,一个采用缓冲区参数的 readinto() 方法和一个不需要参数的 readline() 方法,如 [ X211X]io.BufferedIOBase 接口。 因此,file 可以是为二进制读取而打开的磁盘文件、io.BytesIO 对象或任何其他符合此接口的自定义对象。
可选参数 fix_imports、encoding 和 errors 用于控制对 Python 2 生成的 pickle 流的兼容性支持。 如果 fix_imports 为真,pickle 将尝试将旧的 Python 2 名称映射到 Python 3 中使用的新名称。 encoding 和 errors 告诉 pickle 如何解码 Python 2 腌制的 8 位字符串实例; 这些分别默认为 'ASCII' 和 'strict'。 encoding 可以是 'bytes' 来读取这些 8 位字符串实例作为字节对象。 需要使用
encoding='latin1'
来取消 NumPy 数组和 datetime、date 和 time 由 Python 2 酸洗的实例。如果 buffers 为 None(默认值),则反序列化所需的所有数据都必须包含在 pickle 流中。 这意味着 buffer_callback 参数在 Pickler 被实例化时(或当 dump() 或 dumps() 被调用时)为 None .
如果 buffers 不是 None,则它应该是启用缓冲区的对象的迭代,每次 pickle 流引用 带外 缓冲区视图时都会消耗该对象。 这些缓冲区是为了给 Pickler 对象的 buffer_callback 提供的。
3.8 版更改: 添加了 buffers 参数。
- load()
从构造函数中给出的打开文件对象中读取对象的腌制表示,并返回其中指定的重构对象层次结构。 超过对象的pickle 表示的字节将被忽略。
- persistent_load(pid)
默认情况下引发 UnpicklingError。
如果已定义,persistent_load() 应返回由持久 ID pid 指定的对象。 如果遇到无效的持久 ID,则应引发 UnpicklingError。
有关详细信息和使用示例,请参阅 外部对象持久性 。
- find_class(module, name)
如有必要,导入 module 并从中返回名为 name 的对象,其中 module 和 name 参数为 str ] 对象。 请注意,与其名称所暗示的不同,find_class() 也用于查找函数。
子类可以覆盖它以控制对象的类型和加载方式,从而潜在地降低安全风险。 有关详细信息,请参阅 限制全局变量 。
- class pickle.PickleBuffer(buffer)
表示可腌制数据的缓冲区的包装器。 buffer 必须是一个 buffer-providing 对象,比如一个 bytes-like object 或者一个 N 维数组。
PickleBuffer 本身就是一个缓冲区提供者,因此可以将它传递给其他需要缓冲区提供对象的 API,例如 memoryview。
PickleBuffer 对象只能使用 pickle 协议 5 或更高版本进行序列化。 它们有资格进行 带外序列化 。
3.8 版中的新功能。
- raw()
返回此缓冲区下的内存区域的 memoryview。 返回的对象是格式为
B
(无符号字节)的一维 C 连续内存视图。 BufferError 如果缓冲区既不是 C 连续的,也不是 Fortran 连续的,则会引发。
- release()
释放由 PickleBuffer 对象公开的底层缓冲区。
什么可以腌制和不腌制?
可以腌制以下类型:
None
、True
和False
- 整数、浮点数、复数
- 字符串、字节、字节数组
- 仅包含可picklable 对象的元组、列表、集合和字典
- 在模块顶层定义的函数(使用 def,而不是 lambda)
- 定义在模块顶层的内置函数
- 在模块顶层定义的类
- 其 __dict__ 或调用
__getstate__()
的结果是可酸洗的类的实例(有关详细信息,请参阅 酸洗类实例 部分)。
尝试腌制不可腌制的对象将引发 PicklingError 异常; 发生这种情况时,可能已经将未指定数量的字节写入底层文件。 尝试腌制一个高度递归的数据结构可能会超过最大递归深度,在这种情况下将引发 RecursionError。 您可以使用 sys.setrecursionlimit() 小心地提高此限制。
请注意,函数(内置和用户定义的)由“完全限定”名称引用而不是值来腌制。 2 这意味着只腌制函数名称,以及定义函数的模块名称。 函数的代码及其任何函数属性都不会被腌制。 因此定义模块必须在 unpickling 环境中可导入,并且模块必须包含命名对象,否则将引发异常。 3
类似地,类通过命名引用进行酸洗,因此在 unpickling 环境中适用相同的限制。 请注意,类的代码或数据均未经过酸洗,因此在以下示例中,类属性 attr
在 unpickling 环境中不会恢复:
class Foo:
attr = 'A class attribute'
picklestring = pickle.dumps(Foo)
这些限制就是为什么必须在模块的顶层定义可腌制的函数和类。
类似地,当类实例被腌制时,它们的类的代码和数据不会与它们一起被腌制。 只有实例数据被腌制。 这是故意完成的,因此您可以修复类中的错误或向类添加方法,并且仍然加载使用早期版本的类创建的对象。 如果您计划拥有将看到一个类的多个版本的长期对象,那么在对象中放置一个版本号可能是值得的,以便可以通过类的 __setstate__()
方法进行适当的转换。
酸洗类实例
在本节中,我们将描述可用于定义、自定义和控制类实例的pickle 和unpickle 方式的一般机制。
在大多数情况下,不需要额外的代码来使实例可以pickle。 默认情况下,pickle 将通过自省检索实例的类和属性。 当一个类实例被 unpickled 时,它的 __init__()
方法通常是 not 调用。 默认行为首先创建一个未初始化的实例,然后恢复保存的属性。 以下代码显示了此行为的实现:
def save(obj):
return (obj.__class__, obj.__dict__)
def load(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
,该元组将在解压时传递给 __new__() 方法。如果定义了 __getnewargs_ex__(),则不会调用 __getnewargs__()。
3.6 版更改: 在 Python 3.6 之前, __getnewargs__() 在协议 2 和 3 中被调用而不是 __getnewargs_ex__()。
- object.__getstate__()
- 类可以进一步影响它们的实例如何被腌制; 如果类定义了 __getstate__() 方法,则调用该方法并将返回的对象作为实例的内容进行腌制,而不是实例字典的内容。 如果 __getstate__() 方法不存在,则实例的 __dict__ 像往常一样被腌制。
- object.__setstate__(state)
在 unpickling 时,如果该类定义了 __setstate__(),则以 unpickled 状态调用它。 在这种情况下,状态对象不需要是字典。 否则,pickled 状态必须是一个字典,并且它的项被分配给新实例的字典。
笔记
如果 __getstate__() 返回一个假值,__setstate__() 方法在 unpickling 时不会被调用。
有关如何使用方法 __getstate__()
和 __setstate__()
的更多信息,请参阅 处理有状态对象 部分。
笔记
在 unpickling 时,可能会在实例上调用一些方法,如 __getattr__()
、__getattribute__()
或 __setattr__()
。 如果这些方法依赖于某些内部不变量为真,则该类型应实现 __new__()
来建立这样的不变量,因为在取消实例时不会调用 __init__()
。
正如我们将看到的,pickle 不直接使用上述方法。 事实上,这些方法是实现 __reduce__()
特殊方法的复制协议的一部分。 复制协议提供了一个统一的接口,用于检索酸洗和复制对象所需的数据。 4
尽管功能强大,但直接在类中实现 __reduce__()
很容易出错。 因此,类设计者应尽可能使用高级接口(即 __getnewargs_ex__()
、__getstate__()
和 __setstate__()
)。 然而,我们将展示使用 __reduce__()
是唯一选择或导致更有效酸洗或两者兼而有之的情况。
- object.__reduce__()
该接口目前定义如下。 __reduce__() 方法不接受任何参数,并且应该返回一个字符串或者最好是一个元组(返回的对象通常被称为“reduce 值”)。
如果返回字符串,则该字符串应被解释为全局变量的名称。 它应该是对象相对于其模块的本地名称; pickle 模块搜索模块命名空间以确定对象的模块。 此行为通常对单身人士有用。
返回元组时,它的长度必须介于 2 到 6 个项目之间。 可以省略可选项目,也可以提供
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 版本提供向后兼容的 reduce 值。
外部对象的持久性
为了对象持久性的好处,pickle 模块支持对pickle 数据流之外的对象的引用的概念。 此类对象由持久 ID 引用,该 ID 应该是一串字母数字字符(对于协议 0)5 或只是一个任意对象(对于任何更新的协议)。
pickle 模块未定义此类持久 ID 的解析; 它将将此分辨率委托给pickler 和unpickler 上的用户定义方法,分别为persistent_id() 和persistent_load()。
要pickle具有外部持久ID的对象,pickler必须有一个自定义的persistent_id()方法,该方法将一个对象作为参数并返回None
或该对象的持久ID。 当返回 None
时,pickler 只是像平常一样腌制对象。 当返回一个持久 ID 字符串时,pickler 将pickle 该对象以及一个标记,以便 unpickler 将其识别为持久 ID。
要解开外部对象,解开器必须有一个自定义的 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()
调度表
如果您想自定义某些类的酸洗而不干扰任何其他依赖酸洗的代码,那么您可以创建一个带有私有调度表的酸洗器。
由 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 模块的等效代码是
copyreg.pickle(SomeClass, reduce_SomeClass)
f = io.BytesIO()
p = pickle.Pickler(f)
处理有状态对象
这是一个示例,展示了如何修改类的酸洗行为。 TextReader
类打开一个文本文件,并在每次调用其 readline()
方法时返回行号和行内容。 如果一个 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 可能不够灵活。 特别是我们可能希望根据对象类型以外的其他标准自定义酸洗,或者我们可能希望自定义函数和类的酸洗。
对于这些情况,可以从 Picker 类继承子类并实现 reducer_override() 方法。 这个方法可以返回一个任意的归约元组(见 __reduce__()
)。 它也可以返回 NotImplemented
以回退到传统行为。
如果同时定义了 dispatch_table 和 reducer_override(),那么 reducer_override() 方法优先。
笔记
出于性能原因,可能不会为以下对象调用 reducer_override():None
、True
、False
和 的确切实例int, float, bytes, str, dict, set, X257X]、 列表 和 元组。
这是一个简单的例子,我们允许酸洗和重建给定的类:
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 流复制数据。
如果 provider(要传输的对象类型的实现)和 consumer(通信系统的实现)都支持带外pickle 协议 5 及更高版本提供的传输设施。
提供者 API
要被pickle的大数据对象必须实现一个专用于协议5及更高版本的__reduce_ex__()
方法,该方法返回一个PickleBuffer实例(而不是例如 一个 bytes 对象)用于任何大数据。
PickleBuffer 对象 发出信号 表明底层缓冲区有资格进行带外数据传输。 这些对象与 pickle 模块的正常使用保持兼容。 但是,消费者也可以选择告诉 pickle 他们将自己处理这些缓冲区。
消费者API
通信系统可以启用对序列化对象图时生成的 PickleBuffer 对象的自定义处理。
在发送端,它需要将 buffer_callback 参数传递给 Pickler(或 dump() 或 dumps() 函数),它将在酸洗对象图时生成的每个 PickleBuffer 被调用。 buffer_callback 累积的缓冲区不会看到它们的数据复制到pickle 流中,只会插入一个廉价的标记。
在接收端,它需要将 buffers 参数传递给 Unpickler(或 load() 或 loads() 函数),这是传递给 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 数组)没有此限制,并且在不同的进程或系统之间传输时允许使用零副本酸洗(或制作尽可能少的副本)。
限制全局变量
默认情况下,unpickling 将导入它在 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() 来控制什么被取消。 与其名称所暗示的不同,只要请求全局(即类或函数),就会调用 Unpickler.find_class()。 因此,可以完全禁止全局变量或将它们限制为安全子集。
下面是一个 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
正如我们的例子所示,你必须小心你允许去腌制的东西。 因此,如果担心安全性,您可能需要考虑替代方案,例如 xmlrpc.client 中的编组 API 或第三方解决方案。
例子
对于最简单的代码,使用 dump() 和 load() 函数。
import pickle
# An arbitrary collection of objects supported by pickle.
data = {
'a': [1, 2.0, 3, 4+6j],
'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)
也可以看看
- 模块 copyreg
- 扩展类型的 Pickle 接口构造函数注册。
- 模块 pickletools
- 用于处理和分析腌制数据的工具。
- 模块 搁架
- 对象的索引数据库; 使用 pickle。
- 模块复制
- 浅层和深层对象复制。
- 模块元帅
- 内置类型的高性能序列化。
脚注
- 1
- 不要将其与 marshal 模块混淆
- 2
- 这就是为什么 lambda 函数不能被腌制的原因:所有
lambda
函数共享同一个名称:<lambda>
。 - 3
- 引发的异常可能是 ImportError 或 AttributeError,但也可能是别的东西。
- 4
- copy 模块使用这个协议进行浅拷贝和深拷贝操作。
- 5
- 对字母数字字符的限制是因为协议 0 中的持久 ID 由换行符分隔。 因此,如果持久 ID 中出现任何类型的换行符,则生成的 pickle 将变得不可读。