12.1. pickle —— Python 对象序列化

源代码: Lib/pickle.py


模块 pickle 实现了对一个 Python 对象结构的二进制序列化和反序列化。 “Pickling” 是将 Python 对象及其所拥有的层次结构转化为一个字节流的过程,而 “unpickling” 是相反的操作,会将(来自一个 binary file 或者 bytes-like object 的)字节流转化回一个对象层次结构。Pickling(和 unpickling)也被称为“序列化”, “编组” [1] 或者 “平面化”。而为了避免混乱,此处采用术语 “pickling” 和 “unpickling”。

警告

pickle 模块在接受被错误地构造或者被恶意地构造的数据时不安全。永远不要 unpickle 来自于不受信任的或者未经验证的来源的数据。

12.1.1. 与其他 Python 模块间的关系

12.1.1.1. 与 marshal 间的关系

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

pickle 模块与 marshal 在如下几方面显著地不同:

  • pickle 模块会跟踪已被序列化的对象,所以该对象之后再次被引用时不会再次被序列化。marshal 不会这么做。

    这隐含了递归对象和共享对象。递归对象指包含对自己的引用的对象。这种对象并不会被 marshal 接受,并且实际上尝试 marshal 递归对象会让你的 Python 解释器崩溃。对象共享发生在对象层级中存在多处引用同一对象时。pickle 只会存储这些对象一次,并确保其他的引用指向同一个主副本。共享对象将保持共享,这可能对可变对象非常重要。

  • marshal 不能被用于序列化用户定义类及其实例。pickle 能够透明地存储并保存类实例,然而此时类定义必须能够从与被存储时相同的模块被引入。

  • The marshal serialization format is not guaranteed to be portable across Python versions. Because its primary job in life is to support .pyc files, the Python implementers reserve the right to change the serialization format in non-backwards compatible ways should the need arise. The pickle serialization format is guaranteed to be backwards compatible across Python releases.

12.1.1.2. 与 json 模块的比较

Pickle 协议和 JSON (JavaScript Object Notation) 间有着本质的不同:

  • JSON 是一个文本序列化格式(它输出 unicode 文本,尽管在大多数时候它会接着以 utf-8 编码),而 pickle 是一个二进制序列化格式;
  • JSON 是我们可以直观阅读的,而 pickle 不是;
  • JSON是可互操作的,在Python系统之外广泛使用,而pickle则是Python专用的;
  • 默认情况下,JSON 只能表示 Python 内置类型的子集,不能表示自定义的类;但 pickle 可以表示大量的 Python 数据类型(可以合理使用 Python 的对象内省功能自动地表示大多数类型,复杂情况可以通过实现 specific object APIs 来解决)。

参见

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

12.1.2. 数据流格式

pickle 所使用的数据格式仅可用于 Python。这样做的好处是没有外部标准给该格式强加限制,比如 JSON 或 XDR(不能表示共享指针)标准;但这也意味着非 Python 程序可能无法重新读取 pickle 打包的 Python 对象。

默认情况下,pickle 格式使用相对紧凑的二进制来存储。如果需要让文件更小,可以高效地 压缩 由 pickle 打包的数据。

pickletools 模块包含了相应的工具用于分析 pickle 生成的数据流。pickletools 源码中包含了对 pickle 协议使用的操作码的大量注释。

当前用于 pickling 的协议共有 5 种。使用的协议版本越高,读取生成的 pickle 所需的 Python 版本就要越新。

  • v0 版协议是原始的“人类可读”协议,并且向后兼容早期版本的 Python。
  • v1 版协议是较早的二进制格式,它也与早期版本的 Python 兼容。
  • v2 版协议是在 Python 2.3 中引入的。它为存储 new-style class 提供了更高效的机制。欲了解有关第 2 版协议带来的改进,请参阅 PEP 307
  • v3 版协议添加于 Python 3.0。它具有对 bytes 对象的显式支持,且无法被 Python 2.x 打开。这是目前默认使用的协议,也是在要求与其他 Python 3 版本兼容时的推荐协议。
  • v4 版协议添加于 Python 3.4。它支持存储非常大的对象,能存储更多种类的对象,还包括一些针对数据格式的优化。有关第 4 版协议带来改进的信息,请参阅 PEP 3154

注解

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

12.1.3. 模块接口

要序列化某个包含层次结构的对象,只需调用 dumps() 函数即可。同样,要反序列化数据流,可以调用 loads() 函数。但是,如果要对序列化和反序列化加以更多的控制,可以分别创建 PicklerUnpickler 对象。

pickle 模块包含了以下常量:

pickle.HIGHEST_PROTOCOL

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

pickle.DEFAULT_PROTOCOL

一个整数,表示封存操作使用的 协议版本。 它可能小于 HIGHEST_PROTOCOL。 当前默认协议版本为 3,它是一个为 Python 3 设计的新协议。

pickle 模块提供了以下方法,让打包过程更加方便。

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

Write a pickled representation of obj to the open file object file. This is equivalent to Pickler(file, protocol).dump(obj).

可选参数 protocol 是一个整数,告知 pickler 使用指定的协议,可选择的协议范围从 0 到 HIGHEST_PROTOCOL。如果没有指定,这一参数默认值为 DEFAULT_PROTOCOL。指定一个负数就相当于指定 HIGHEST_PROTOCOL

参数 file 必须有一个 write() 方法,该 write() 方法要能接收字节作为其唯一参数。因此,它可以是一个打开的磁盘文件(用于写入二进制内容),也可以是一个 io.BytesIO 实例,也可以是满足这一接口的其他任何自定义对象。

如果 fix_imports 为 True 且 protocol 小于 3,pickle 将尝试将 Python 3 中的新名称映射到 Python 2 中的旧模块名称,因此 Python 2 也可以读取打包出的数据流。

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

Return the pickled representation of the object as a bytes object, instead of writing it to a file.

参数 protocolfix_imports 的含义与它们在 dump() 中的含义相同。

pickle.load(file, *, fix_imports=True, encoding="ASCII", errors="strict")

Read a pickled object representation from the open file object file and return the reconstituted object hierarchy specified therein. This is equivalent to Unpickler(file).load().

The protocol version of the pickle is detected automatically, so no protocol argument is needed. Bytes past the pickled object’s representation are ignored.

参数 file 必须有两个方法,其中 read() 方法接受一个整数参数,而 readline() 方法不需要参数。 两个方法都应返回字节串。 因此 file 可以是一个打开用于二进制读取的磁盘文件、一个 io.BytesIO 对象,或者任何满足此接口要求的其他自定义对象。

可选的关键字参数有 fix_imports, encodingerrors,它们用于控制由 Python 2 所生成 pickle 流的兼容性支持。 如果 fix_imports 为真值,则 pickle 将尝试把旧的 Python 2 名称映射到 Python 3 所使用的新名称。 encodingerrors 将告知 pickle 如何解码由 Python 2 所封存的 8 位字符串实例;这两个参数的默认值分别为 ‘ASCII’ 和 ‘strict’。 encoding 可设为 ‘bytes’ 以将这些 8 位字符串实例作为字节对象来读取。

pickle.loads(bytes_object, *, fix_imports=True, encoding="ASCII", errors="strict")

Read a pickled object hierarchy from a bytes object and return the reconstituted object hierarchy specified therein.

The protocol version of the pickle is detected automatically, so no protocol argument is needed. Bytes past the pickled object’s representation are ignored.

可选的关键字参数有 fix_imports, encodingerrors,它们用于控制由 Python 2 所生成 pickle 流的兼容性支持。 如果 fix_imports 为真值,则 pickle 将尝试把旧的 Python 2 名称映射到 Python 3 所使用的新名称。 encodingerrors 将告知 pickle 如何解码由 Python 2 所封存的 8 位字符串实例;这两个参数的默认值分别为 ‘ASCII’ 和 ‘strict’。 encoding 可设为 ‘bytes’ 以将这些 8 位字符串实例作为字节对象来读取。

pickle 模块定义了以下 3 个异常:

exception pickle.PickleError

其他 pickle 异常的基类。它是 Exception 的一个子类。

exception pickle.PicklingError

Pickler 遇到无法解包的对象时抛出此错误。它是 PickleError 的子类。

参考 可以被打包/解包的对象 来了解哪些对象可以被打包。

exception pickle.UnpicklingError

当解包出错时抛出此异常,例如数据损坏或对象不安全。它是 PickleError 的子类。

注意,解包时可能还会抛出其他异常,包括(但不限于) AttributeError、EOFError、ImportError 和 IndexError。

pickle 模块可导出两个类,PicklerUnpickler:

class pickle.Pickler(file, protocol=None, *, fix_imports=True)

它接受一个二进制文件用于写入 pickle 数据流。

可选参数 protocol 是一个整数,告知 pickler 使用指定的协议,可选择的协议范围从 0 到 HIGHEST_PROTOCOL。如果没有指定,这一参数默认值为 DEFAULT_PROTOCOL。指定一个负数就相当于指定 HIGHEST_PROTOCOL

参数 file 必须有一个 write() 方法,该 write() 方法要能接收字节作为其唯一参数。因此,它可以是一个打开的磁盘文件(用于写入二进制内容),也可以是一个 io.BytesIO 实例,也可以是满足这一接口的其他任何自定义对象。

如果 fix_imports 为 True 且 protocol 小于 3,pickle 将尝试将 Python 3 中的新名称映射到 Python 2 中的旧模块名称,因此 Python 2 也可以读取打包出的数据流。

dump(obj)

Write a pickled representation of obj to the open file object given in the constructor.

persistent_id(obj)

默认什么也不做。它存在是为了让子类可以重载它。

如果 persistent_id() 返回 Noneobj 会被照常 pickle。如果返回其他值,Pickler 会将这个函数的返回值作为 obj 的持久化 ID(Pickler 本应得到序列化数据流并将其写入文件,若此函数有返回值,则得到此函数的返回值并写入文件)。这个持久化 ID 的解释应当定义在 Unpickler.persistent_load() 中(该方法定义还原对象的过程,并返回得到的对象)。注意,persistent_id() 的返回值本身不能拥有持久化 ID。

参阅 持久化外部对象 获取详情和使用示例。

dispatch_table

Pickler 对象的 dispatch 表是 copyreg.pickle() 中用到的 reduction 函数 的注册。dispatch 表本身是一个 class 到其 reduction 函数的映射键值对。一个 reduction 函数只接受一个参数,就是其关联的 class,函数行为应当遵守 __reduce__() 接口规范。

Pickler 对象默认并没有 dispatch_table 属性,该对象默认使用 copyreg 模块中定义的全局 dispatch 表。如果要为特定 Pickler 对象自定义序列化过程,可以将 dispatch_table 属性设置为类字典对象(dict-like object)。另外,如果 Pickler 的子类设置了 dispatch_table 属性,则该子类的实例会使用这个表作为默认的 dispatch 表。

参阅 Dispatch 表 获取使用示例。

3.3 新版功能.

fast

已弃用。设为 True 则启用快速模式。快速模式禁用了“备忘录” (memo) 的使用,即不生成多余的 PUT 操作码来加快打包过程。不应将其与自指 (self-referential) 对象一起使用,否则将导致 Pickler 无限递归。

如果需要进一步提高 pickle 的压缩率,请使用 pickletools.optimize()

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

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

Pickle 协议版本是自动检测出来的,所以不需要参数来指定协议。

参数 file 必须有两个方法,其中 read() 方法接受一个整数参数,而 readline() 方法不需要参数。 两个方法都应返回字节串。 因此 file 可以是一个打开用于二进制读取的磁盘文件对象、一个 io.BytesIO 对象,或者任何满足此接口要求的其他自定义对象。

Optional keyword arguments are fix_imports, encoding and errors, which are used to control compatibility support for pickle stream generated by Python 2. If fix_imports is true, pickle will try to map the old Python 2 names to the new names used in Python 3. The encoding and errors tell pickle how to decode 8-bit string instances pickled by Python 2; these default to ‘ASCII’ and ‘strict’, respectively. The encoding can be ‘bytes’ to read these ß8-bit string instances as bytes objects.

load()

Read a pickled object representation from the open file object given in the constructor, and return the reconstituted object hierarchy specified therein. Bytes past the pickled object’s representation are ignored.

persistent_load(pid)

默认抛出 UnpicklingError 异常。

如果定义了此方法,persistent_load() 应当返回持久化 ID pid 所指定的对象。 如果遇到无效的持久化 ID,则应当引发 UnpicklingError

参阅 持久化外部对象 获取详情和使用示例。

find_class(module, name)

如有必要,导入 module 模块并返回其中名叫 name 的对象,其中 modulename 参数都是 str 对象。注意,不要被这个函数的名字迷惑,find_class() 同样可以用来导入函数。

子类可以重载此方法,来控制加载对象的类型和加载对象的方式,从而尽可能降低安全风险。参阅 限制全局变量 获取更详细的信息。

12.1.4. 可以被打包/解包的对象

下列类型可以被打包:

  • NoneTrueFalse
  • 整数、浮点数、复数
  • str、byte、bytearray
  • 只包含可打包对象的集合,包括 tuple、list、set 和 dict
  • 定义在模块顶层的函数(使用 def 定义,lambda 函数则不可以)
  • 定义在模块顶层的内置函数
  • 定义在模块顶层的类
  • 某些类实例,这些类的 __dict__ 属性值或 __getstate__() 函数的返回值可以被打包(详情参阅 打包类实例 这一段)。

尝试打包不能被打包的对象会抛出 PicklingError 异常,异常发生时,可能有部分字节已经被写入指定文件中。尝试打包递归层级很深的对象时,可能会超出最大递归层级限制,此时会抛出 RecursionError 异常,可以通过 sys.setrecursionlimit() 调整递归层级,不过请谨慎使用这个函数,因为可能会导致解释器崩溃。

注意,函数(内建函数或用户自定义函数)在被打包时,引用的是函数全名。[2] 这意味着只有函数所在的模块名,与函数名会被打包,函数体及其属性不会被打包。因此,在解包的环境中,函数所属的模块必须是可以被导入的,而且模块必须包含这个函数被打包时的名称,否则会抛出异常。[3]

同样的,类也只打包名称,所以在解包环境中也有和函数相同的限制。注意,类体及其数据不会被打包,所以在下面的例子中类属性 attr 不会存在于解包后的环境中:

class Foo:
    attr = 'A class attribute'

picklestring = pickle.dumps(Foo)

这些限制决定了为什么必须在一个模块的顶层定义可打包的函数和类。

类似的,在打包类的实例时,其类体和类数据不会跟着实例一起被打包,只有实例数据会被打包。这样设计是有目的的,在将来修复类中的错误、给类增加方法之后,仍然可以载入原来版本类实例的打包数据来还原该实例。如果你准备长期使用一个对象,可能会同时存在较多版本的类体,可以为对象添加版本号,这样就可以通过类的 __setstate__() 方法将老版本转换成新版本。

12.1.5. 打包类实例

在本节中,我们描述了可用于定义、自定义和控制如何打包和解包类实例的通用流程。

通常,使一个实例可被打包不需要附加任何代码。Pickle 默认会通过 Python 的内省机制获得实例的类及属性。而当实例解包时,它的 __init__() 方法通常 不会 被调用。其默认动作是:先创建一个未初始化的实例,然后还原其属性,下面的代码展示了这种行为的实现机制:

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

In protocols 4 and newer, classes that implements the __getnewargs_ex__() method can dictate the values passed to the __new__() method upon unpickling. The method must return a pair (args, kwargs) where args is a tuple of positional arguments and kwargs a dictionary of named arguments for constructing the object. Those will be passed to the __new__() method upon unpickling.

如果类的 __new__() 方法只接受关键字参数,则应当实现这个方法。否则,为了兼容性,更推荐实现 __getnewargs__() 方法。

object.__getnewargs__()

This method serve a similar purpose as __getnewargs_ex__() but for protocols 2 and newer. It must return a tuple of arguments args which will be passed to the __new__() method upon unpickling.

In protocols 4 and newer, __getnewargs__() will not be called if __getnewargs_ex__() is defined.

object.__getstate__()

类还可以进一步控制其实例的打包过程。如果类定义了 __getstate__(),它就会被调用,其返回的对象是被当做实例内容来打包的,否则打包的是实例的 __dict__。如果 __getstate__() 未定义,实例的 __dict__ 会被照常打包。

object.__setstate__(state)

当解包时,如果类定义了 __setstate__(),就会在已解包状态下调用它。此时不要求实例的 state 对象必须是 dict。没有定义此方法的话,先前打包的 state 对象必须是 dict,且该 dict 内容会在解包时赋给新实例的 __dict__。

注解

如果 __getstate__() 返回 False,那么在解包时就不会调用 __setstate__() 方法。

参考 处理有状态的对象 一段获取如何使用 __getstate__()__setstate__() 方法的更多信息。

注解

At unpickling time, some methods like __getattr__(), __getattribute__(), or __setattr__() may be called upon the instance. In case those methods rely on some internal invariant being true, the type should implement __getnewargs__() or __getnewargs_ex__() to establish such an invariant; otherwise, neither __new__() nor __init__() will be called.

可以看出,其实 pickle 并不直接调用上面的几个函数。事实上,这几个函数是复制协议的一部分,它们实现了 __reduce__() 这一特殊接口。复制协议提供了统一的接口,用于在打包或复制对象的过程中取得所需数据。[4]

尽管这个协议功能很强,但是直接在类中实现 __reduce__() 接口容易产生错误。因此,设计类时应当尽可能的使用高级接口(比如 __getnewargs_ex__()__getstate__()__setstate__())。后面仍然可以看到直接实现 __reduce__() 接口的状况,可能别无他法,可能为了获得更好的性能,或者两者皆有之。

object.__reduce__()

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

如果返回字符串,该字符串会被当做一个全局变量的名称。它应该是对象相对于其模块的本地名称,pickle 模块会搜索模块命名空间来确定对象所属的模块。这种行为常在单例模式使用。

当返回的是一个元组时,它的长度必须在二至五项之间。 可选项可以被省略或将值设为 None。 每项的语义分别如下所示:

  • 一个可调用对象,该对象会在创建对象的最初版本时调用。
  • 可调用对象的参数,是一个元组。如果可调用对象不接受参数,必须提供一个空元组。
  • 可选元素,用于表示对象的状态,将被传给前述的 __setstate__() 方法。 如果对象没有此方法,则这个元素必须是字典类型,并会被添加至 __dict__ 属性中。
  • 可选元素,一个返回连续项的迭代器(而不是序列)。这些项会被 obj.append(item) 逐个加入对象,或被 obj.extend(list_of_items) 批量加入对象。这个元素主要用于 list 的子类,也可以用于那些正确实现了 append()extend() 方法的类。(具体是使用 append() 还是 extend() 取决于 pickle 协议版本以及待插入元素的项数,所以这两个方法必须同时被类支持。)
  • 可选元素,一个返回连续键值对的迭代器(而不是序列)。这些键值对将会以 obj[key] = value 的方式存储于对象中。该元素主要用于 dict 子类,也可以用于那些实现了 __setitem__() 的类。
object.__reduce_ex__(protocol)

作为替代选项,也可以实现 __reduce_ex__() 方法。 此方法的唯一不同之处在于它应接受一个整型参数用于指定协议版本。 如果定义了这个函数,则会覆盖 __reduce__() 的行为。 此外,__reduce__() 方法会自动成为扩展版方法的同义词。 这个函数主要用于为以前的 Python 版本提供向后兼容的 reduce 值。

12.1.5.1. 持久化外部对象

为了获取对象持久化的利益, pickle 模块支持引用已封存数据流之外的对象。 这样的对象是通过一个持久化 ID 来引用的,它应当是一个由字母数字类字符组成的字符串 (对于第 0 版协议) [5] 或是一个任意对象 (用于任意新版协议)。

The resolution of such persistent IDs is not defined by the pickle module; it will delegate this resolution to the user defined methods on the pickler and unpickler, persistent_id() and persistent_load() respectively.

To pickle objects that have an external persistent id, the pickler must have a custom persistent_id() method that takes an object as an argument and returns either None or the persistent id for that object. When None is returned, the pickler simply pickles the object as normal. When a persistent ID string is returned, the pickler will pickle that object, along with a marker so that the unpickler will recognize it as a persistent ID.

要解封外部对象,Unpickler 必须实现 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()

12.1.5.2. Dispatch 表

如果想对某些类进行自定义封存,而又不想在类中增加用于封存的代码,就可以创建带有特殊 dispatch 表的 pickler。

copyreg 模块的 copyreg.dispatch_table 中定义了全局 dispatch 表。因此,可以使用 copyreg.dispatch_table 修改后的副本作为自有 dispatch 表。

例如:

f = io.BytesIO()
p = pickle.Pickler(f)
p.dispatch_table = copyreg.dispatch_table.copy()
p.dispatch_table[SomeClass] = reduce_SomeClass

创建了一个带有自有 dispatch 表的 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 的实例都会共用同一份 dispatch 表。使用 copyreg 模块实现的等效代码是:

copyreg.pickle(SomeClass, reduce_SomeClass)
f = io.BytesIO()
p = pickle.Pickler(f)

12.1.5.3. 处理有状态的对象

下面的示例展示了如何修改类在封存时的行为。其中 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!'

12.1.6. 限制全局变量

默认情况下,解封将会导入在 pickle 数据中找到的任何类或函数。 对于许多应用来说,此行为是不可接受的,因为它会允许解封器导入并发起调用任意代码。 只须考虑当这个手工构建的 pickle 数据流被加载时会做什么:

>>> 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 或是第三方解决方案。

12.1.7. 性能

较新版本的 pickle 协议(第 2 版或更高)具有针对某些常见特性和内置类型的高效二进制编码格式。 此外,pickle 模块还拥有一个以 C 编写的透明优化器。

12.1.8. 例子

对于最简单的代码,请使用 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
用于处理和分析已打包数据的工具。
模块 shelve
带索引的数据库,用于存放对象,使用了 pickle 模块。
模块 copy
浅层 (shallow) 和深层 (deep) 复制对象操作
模块 marshal
高效地序列化内置类型的数据。

备注

[1]不要把它与 marshal 模块混淆。
[2]This is why lambda functions cannot be pickled: all lambda functions share the same name: <lambda>.
[3]抛出的异常有可能是 ImportErrorAttributeError,也可能是其他异常。
[4]copy 模块使用这一协议实现浅层 (shallow) 和深层 (deep) 复制操作。
[5]对字母数字类字符的限制是由于持久化 ID 在协议版本 0 中是由分行符来分隔的。 因此如果持久化 ID 中出现任何形式的分行符,封存结果就将变得无法读取。