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
不会这样做。这对递归对象和对象共享都有影响。递归对象是包含对其自身的引用的对象。这些对象无法由 marshal 处理,事实上,尝试对递归对象进行 marshal 操作会导致 Python 解释器崩溃。当在被序列化的对象层次结构中的不同位置存在对同一对象的多个引用时,就会发生对象共享。
pickle
只存储一次此类对象,并确保所有其他引用都指向主副本。共享对象保持共享,这对于可变对象来说非常重要。marshal
不能用于序列化用户定义的类及其实例。pickle
可以透明地保存和恢复类实例,但是类定义必须是可导入的,并且与存储对象时位于同一个模块中。不保证
marshal
序列化格式在不同 Python 版本之间可移植。因为它的主要工作是支持.pyc
文件,所以如果需要,Python 实现者保留以非向后兼容的方式更改序列化格式的权利。如果您的数据跨越了 Python 2 到 Python 3 类型的独特突破性变化语言边界,则pickle
序列化格式保证在 Python 版本之间向后兼容,前提是选择了兼容的 pickle 协议,并且 pickling 和 unpickling 代码处理了 Python 2 到 Python 3 类型的差异。
与 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 或 XDR 等外部标准的限制(这些标准不能表示指针共享);然而,这也意味着非 Python 程序可能无法重建 pickle 化的 Python 对象。
默认情况下,pickle
数据格式使用相对紧凑的二进制表示形式。如果您需要最佳的大小特性,可以有效地 压缩 pickle 化的数据。
模块 pickletools
包含用于分析 pickle
生成的的数据流的工具。pickletools
源代码包含关于 pickle 协议使用的操作码的详细注释。
目前有 6 种不同的协议可用于 pickle。使用的协议版本越高,读取生成的 pickle 所需的 Python 版本就越新。
协议版本 0 是最初的“人类可读”协议,并且向后兼容早期版本的 Python。
协议版本 1 是一种旧的二进制格式,也与早期版本的 Python 兼容。
协议版本 2 是在 Python 2.3 中引入的。它为 新式类 提供了更高效的 pickle 化。有关协议 2 所带来的改进的信息,请参阅 PEP 307。
协议版本 3 是在 Python 3.0 中添加的。它对
bytes
对象有明确的支持,并且不能被 Python 2.x 反序列化。这是 Python 3.0-3.7 中的默认协议。协议版本 4 是在 Python 3.4 中添加的。它增加了对超大对象的支 持,可以 pickle 更多类型的对象,并对数据格式进行了一些优化。它是从 Python 3.8 开始的默认协议。有关协议 4 所带来的改进的信息,请参阅 PEP 3154。
协议版本 5 是在 Python 3.8 中添加的。它增加了对带外数据的支持,并加快了带内数据的速度。有关协议 5 所带来的改进的信息,请参阅 PEP 574。
模块接口¶
要序列化一个对象层次结构,您只需调用 dumps()
函数。类似地,要反序列化数据流,您可以调用 loads()
函数。但是,如果您想对序列化和反序列化进行更多控制,您可以分别创建一个 Pickler
或 Unpickler
对象。
pickle
模块提供了以下常量
- pickle.DEFAULT_PROTOCOL¶
一个整数,表示用于 pickle 的默认 协议版本。可能小于
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 的 pickle 表示形式写入打开的 文件对象 file。这等效于
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 的 pickle 表示形式,作为一个
bytes
对象,而不是将其写入文件。参数 protocol、fix_imports 和 buffer_callback 的含义与
Pickler
构造函数中的含义相同。在版本 3.8 中更改: 添加了 buffer_callback 参数。
- pickle.load(file, *, fix_imports=True, encoding='ASCII', errors='strict', buffers=None)¶
从打开的 文件对象 file 中读取对象的 pickle 表示形式,并返回其中指定的重构对象层次结构。这等效于
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 必须是 类字节对象。
pickle 的协议版本是自动检测的,因此不需要协议参数。对象 pickle 表示形式之后的字节将被忽略。
参数 fix_imports、encoding、errors、strict 和 buffers 的含义与
Unpickler
构造函数中的含义相同。在 3.8 版更改: 添加了 buffers 参数。
pickle
模块定义了三个异常
- exception pickle.PicklingError¶
当
Pickler
遇到不可 pickle 的对象时引发的错误。它继承自PickleError
。请参阅 哪些对象可以被 pickle 和 unpickle? 以了解哪些类型的对象可以被 pickle。
- exception pickle.UnpicklingError¶
在 unpickle 对象时出现问题(例如数据损坏或安全违规)时引发的错误。它继承自
PickleError
。请注意,在 unpickle 期间也可能引发其他异常,包括(但不限于)AttributeError、EOFError、ImportError 和 IndexError。
pickle
模块导出了三个类,Pickler
、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 中使用的旧模块名称,以便可以使用 Python 2 读取 pickle 数据流。
如果 buffer_callback 为
None
(默认值),则缓冲区视图将作为 pickle 流的一部分序列化到 file 中。如果 buffer_callback 不为
None
,则可以使用缓冲区视图多次调用它。如果回调返回 false 值(例如None
),则给定的缓冲区是 带外 的;否则,缓冲区将在带内序列化,即在 pickle 流内部。如果 buffer_callback 不为
None
且 protocol 为None
或小于 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。有关使用细节和示例,请参阅 外部对象的持久性。
- dispatch_table¶
pickler 对象的调度表是*约简函数*的注册表,其类型可以使用
copyreg.pickle()
声明。它是一个映射,其键是类,其值是约简函数。约简函数接受关联类的单个参数,并且应符合与__reduce__()
方法相同的接口。默认情况下,pickler 对象将没有
dispatch_table
属性,而是使用由copyreg
模块管理的全局调度表。但是,要自定义特定 pickler 对象的 pickle 操作,可以将dispatch_table
属性设置为类似字典的对象。或者,如果Pickler
的子类具有dispatch_table
属性,则这将用作该类实例的默认调度表。有关使用示例,请参阅 调度表。
3.3 版新增。
- reducer_override(obj)¶
可以在
Pickler
子类中定义的特殊约简器。此方法优先于dispatch_table
中的任何约简器。它应该符合与__reduce__()
方法相同的接口,并且可以选择返回NotImplemented
以回退到dispatch_table
注册的约简器来对obj
进行 pickle 操作。有关详细示例,请参阅 类型、函数和其他对象的自定义约简。
3.8 版新增。
- fast¶
已弃用。如果设置为真值,则启用快速模式。快速模式禁用 memo 的使用,因此通过不生成多余的 PUT 操作码来加快 pickle 操作过程。它不应与自引用对象一起使用,否则会导致
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_imports、encoding 和 errors 用于控制对 Python 2 生成的 pickle 流的兼容性支持。如果 fix_imports 为 true,则 pickle 将尝试将旧的 Python 2 名称映射到 Python 3 中使用的新名称。encoding 和 errors 告诉 pickle 如何解码由 Python 2 pickle 操作的 8 位字符串实例;它们分别默认为“ASCII”和“strict”。encoding 可以是“bytes”,以便将这些 8 位字符串实例读取为字节对象。使用
encoding='latin1'
是 unpickle 由 Python 2 pickle 操作的 NumPy 数组和datetime
、date
和time
实例所必需的。如果 buffers 为
None
(默认值),则反序列化所需的所有数据都必须包含在 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
。有关使用细节和示例,请参阅 外部对象的持久性。
- find_class(module, name)¶
如有必要,导入 module 并从中返回名为 name 的对象,其中 module 和 name 参数是
str
对象。请注意,与其名称不同,find_class()
也用于查找函数。子类可以覆盖此方法以控制对象的类型以及如何加载它们,从而潜在地降低安全风险。有关详细信息,请参阅 限制全局变量。
引发带有参数
module
、name
的 审计事件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 对象的元组、列表、集合和字典;
可以从模块顶层访问的类;
调用
__getstate__()
的结果是可 pickle 的此类类的实例(有关详细信息,请参阅 Pickle 类实例 部分)。
尝试 pickle 不可 pickle 的对象将引发 PicklingError
异常;发生这种情况时,可能已经向底层文件写入了一些字节。尝试 pickle 高度递归的数据结构可能会超过最大递归深度,在这种情况下会引发 RecursionError
。您可以使用 sys.setrecursionlimit()
谨慎地提高此限制。
请注意,函数(内置函数和用户定义函数)是通过完全 限定名 进行 pickle 的,而不是通过值进行 pickle 的。[2] 这意味着只 pickle 函数名,以及包含模块和类的名称。函数的代码及其任何函数属性都不会被 pickle。因此,定义模块必须在 unpickle 环境中可导入,并且该模块必须包含命名对象,否则将引发异常。[3]
类似地,类是通过完全限定名进行 pickle 的,因此 unpickle 环境中的相同限制也适用。请注意,类的任何代码或数据都不会被 pickle,因此在以下示例中,类属性 attr
在 unpickle 环境中不会被恢复
class Foo:
attr = 'A class attribute'
picklestring = pickle.dumps(Foo)
这些限制就是为什么可 pickle 的函数和类必须在模块的顶层定义的原因。
类似地,当类实例被 pickle 时,它们的类的代码和数据不会与它们一起被 pickle。只有实例数据会被 pickle。这样做是有意的,因此您可以在类中修复错误或向类添加方法,并且仍然可以加载使用早期版本的类创建的对象。如果您计划拥有将在多个版本的类中使用的长寿命对象,则可能需要在对象中放入版本号,以便类的 __setstate__()
方法可以进行适当的转换。
Pickle 类实例¶
在本节中,我们将介绍可用于定义、自定义和控制如何 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__()
方法的类可以指定在反序列化时传递给__new__()
方法的值。该方法必须返回一个对(args, kwargs)
,其中 args 是一个位置参数元组,kwargs 是一个用于构造对象的命名参数字典。这些参数将在反序列化时传递给__new__()
方法。如果你的类的
__new__()
方法需要仅限关键字的参数,则应实现此方法。否则,为了兼容性,建议实现__getnewargs__()
。在 3.6 版更改:
__getnewargs_ex__()
现在在协议 2 和 3 中使用。
- object.__getnewargs__()¶
此方法的作用与
__getnewargs_ex__()
相似,但仅支持位置参数。它必须返回一个参数元组args
,该元组将在反序列化时传递给__new__()
方法。如果定义了
__getnewargs_ex__()
,则不会调用__getnewargs__()
。在 3.6 版更改: 在 Python 3.6 之前,在协议 2 和 3 中调用的是
__getnewargs__()
而不是__getnewargs_ex__()
。
- object.__getstate__()¶
类可以通过重写
__getstate__()
方法来进一步影响其实例的序列化方式。调用该方法后,返回的对象将作为实例的内容进行序列化,而不是默认状态。有以下几种情况:对于具有实例
__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 模块会搜索模块命名空间以确定对象的模块。此行为通常对单例很有用。
当返回一个元组时,它的长度必须介于 2 到 6 个项之间。可选项目可以省略,也可以提供
None
作为其值。每个项目的语义依次为:一个可调用对象,将调用它来创建对象的初始版本。
可调用对象的参数元组。如果可调用对象不接受任何参数,则必须提供一个空元组。
可选,对象的 state,它将按照前面所述的方式传递给对象的
__setstate__()
方法。如果对象没有这样的方法,则该值必须是一个字典,并且它将被添加到对象的__dict__
属性中。可选,一个迭代器(而不是序列),它会生成连续的项。这些项将使用
obj.append(item)
或批量使用obj.extend(list_of_items)
追加到对象中。这主要用于列表子类,但也可以由其他类使用,只要它们具有 append 和 extend 方法 以及相应的签名。(使用append()
还是extend()
取决于使用的 pickle 协议版本以及要追加的项数,因此必须同时支持两者。)可选,一个迭代器(而不是序列),它会生成连续的键值对。这些项将使用
obj[key] = value
存储到对象中。这主要用于字典子类,但也可以由其他类使用,只要它们实现了__setitem__()
。可选,一个带有
(obj, state)
签名的可调用对象。此可调用对象允许用户以编程方式控制特定对象的 state 更新行为,而不是使用obj
的静态__setstate__()
方法。如果不是None
,则此可调用对象将优先于obj
的__setstate__()
。3.8 版更变: 添加了可选的第六个元组项
(obj, state)
。
- object.__reduce_ex__(protocol)¶
或者,可以定义一个
__reduce_ex__()
方法。唯一的区别是,此方法应该接受一个整数参数,即协议版本。定义后,pickle 将优先使用它而不是__reduce__()
方法。此外,__reduce__()
会自动成为扩展版本的同义词。此方法的主要用途是为旧版本的 Python 提供向后兼容的 reduce 值。
外部对象的持久化¶
为了实现对象持久化,pickle
模块支持对序列化数据流外部的对象的引用概念。此类对象由持久 ID 引用,持久 ID 应该是字母数字字符组成的字符串(对于协议 0)[5] 或任意对象(对于任何更新的协议)。
pickle
模块未定义此类持久 ID 的解析;它会将此解析委托给序列化器和反序列化器上的用户定义方法,分别是 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_table
和 reducer_override()
,则 reducer_override()
方法优先。
注意
出于性能原因,以下对象可能不会调用 reducer_override()
:None
、True
、False
以及 int
、float
、bytes
、str
、dict
、set
、frozenset
、list
和 tuple
的精确实例。
下面是一个简单的示例,我们允许序列化和重建给定的类
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¶
要序列化的较大数据对象必须实现专门针对协议 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
对象的对象的重建器所需的数据。
在发送方和接收方之间,通信系统可以自由地为带外缓冲区实现自己的传输机制。潜在的优化包括使用共享内存或依赖于数据类型的压缩。
示例¶
这是一个简单的例子,我们实现了一个能够参与带外缓冲区 pickle 操作的 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
类方法)返回的缓冲区提供对象的类型正确,则返回该对象。这是在这个玩具示例中模拟零拷贝行为的简单方法。
在消费者端,我们可以用通常的方式 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 的例子,它只允许加载 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)
另请参阅
脚注