13518219792

建站动态

根据您的个性需求进行定制 先人一步 抢占小程序红利时代

创新互联Python教程:pickle—-Python对象序列化

pickle —- python 对象序列化

源代码: Lib/pickle.py

秦皇岛ssl适用于网站、小程序/APP、API接口等需要进行数据传输应用场景,ssl证书未来市场广阔!成为成都创新互联公司的ssl证书销售渠道,可以享受市场价格4-6折优惠!如果有意向欢迎电话联系或者加微信:18982081108(备注:SSL证书合作)期待与您的合作!


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

警告

pickle 模块 并不安全。 你只应该对你信任的数据进行 unpickle 操作。

构建恶意的 pickle 数据来 在解封时执行任意代码 是可能的。 绝对不要对不信任来源的数据和可能被篡改过的数据进行解封。

请考虑使用 hmac 来对数据进行签名,确保数据没有被篡改。

在你处理不信任数据时,更安全的序列化格式如 json 可能更为适合。参见 与 json 模块的比较 。

与其他 Python 模块间的关系

marshal 间的关系

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

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

json 模块的比较

There are fundamental differences between the pickle protocols and JSON (JavaScript Object Notation):

参见

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

数据流格式

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

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

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

当前共有 6 种不同的协议可用于封存操作。 使用的协议版本越高,读取所生成 pickle 对象所需的 Python 版本就要越新。

备注

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

模块接口

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

pickle 模块包含了以下常量:

pickle.HIGHEST_PROTOCOL

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

pickle.DEFAULT_PROTOCOL

整数,用于 pickle 数据的默认 协议版本。它可能小于 HIGHEST_PROTOCOL。当前默认协议是 v4,它在 Python 3.4 中首次引入,与之前的版本不兼容。

在 3.0 版更改: 默认协议版本是 3。

在 3.8 版更改: 默认协议版本是 4。

pickle 模块提供了以下方法,让封存过程更加方便:

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

将对象 obj 封存以后的对象写入已打开的 file object 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*)

obj 封存以后的对象作为 bytes 类型直接返回,而不是将其写入到文件。

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

在 3.8 版更改: 加入了 buffer_callback 参数。

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

从已打开的 file object 文件 中读取封存后的对象,重建其中特定对象的层次结构并返回。它相当于 Unpickler(file).load()

Pickle 协议版本是自动检测出来的,所以不需要参数来指定协议。封存对象以外的其他字节将被忽略。

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

在 3.8 版更改: 加入了 buffers 参数。

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

重建并返回一个对象的封存表示形式 data 的对象层级结构。 data 必须为 bytes-like object。

Pickle 协议版本是自动检测出来的,所以不需要参数来指定协议。封存对象以外的其他字节将被忽略。

参数 fix_imports, encoding, errors, strictbuffers 的含义与在 Unpickler 构造器中的含义相同。

在 3.8 版更改: 加入了 buffers 参数。

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

exception pickle.PickleError

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

exception pickle.PicklingError

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

参考 可以被封存/解封的对象 来了解哪些对象可以被封存。

exception pickle.UnpicklingError

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

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

pickle 模块包含了 3 个类,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() 方法,该 write() 方法要能接收字节作为其唯一参数。因此,它可以是一个打开的磁盘文件(用于写入二进制内容),也可以是一个 io.BytesIO 实例,也可以是满足这一接口的其他任何自定义对象。

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

如果 buffer_callback 为 None(默认情况),缓冲区视图(buffer view)将会作为 pickle 流的一部分被序列化到 file 中。

如果 buffer_callback 不为 None,那它可以用缓冲区视图调用任意次。如果某次调用返回了 False 值(例如 None),则给定的缓冲区是 带外的,否则缓冲区是带内的(例如保存在了 pickle 流里面)。

如果 buffer_callback 不是 None 且 protocol 是 None 或小于 5,就会出错。

在 3.8 版更改: 加入了 buffer_callback 参数。

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, encodingerrors,用于控制由Python 2 生成的 pickle 流的兼容性。如果 fix_imports 为 True,则 pickle 将尝试将旧的 Python 2 名称映射到 Python 3 中对应的新名称。encodingerrors 参数告诉 pickle 如何解码 Python 2 存储的 8 位字符串实例;这两个参数默认分别为 ‘ASCII’ 和 ‘strict’。encoding 参数可置为 ‘bytes’ 来将这些 8 位字符串实例读取为字节对象。读取 NumPy array 和 Python 2 存储的 datetime、date 和 time 实例时,请使用 encoding='latin1'

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

如果 buffers 不为 None,则每次 pickle 流引用 带外 缓冲区视图时,消耗的对象都应该是可迭代的启用缓冲区的对象。这样的缓冲区应该按顺序地提供给 Pickler 对象的 buffer_callback 方法。

在 3.8 版更改: 加入了 buffers 参数。

class pickle.PickleBuffer(buffer)

缓冲区的包装器 (wrapper),缓冲区中包含着可封存的数据。buffer 必须是一个 buffer-providing 对象,比如 bytes-like object 或多维数组。

PickleBuffer 本身就可以生成缓冲区对象,因此可以将其传递给需要缓冲区生成器的其他 API,比如 memoryview。

PickleBuffer 对象只能用 pickle 版本 5 及以上协议进行序列化。它们符合 带外序列化 的条件。

3.8 新版功能.

可以被封存/解封的对象

下列类型可以被封存:

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

请注意(内置与用户自定义的)函数是按完整 qualified name,而不是按值来封存的。 2 这意味着只会封存函数名称,以及包含它的模块和类名称。 函数的代码,以及函数的属性都不会被封存。 因而定义它的模块在解封环境中必须可以被导入,并且模块必须包含所命名的对象,否则将会引发异常。 3

类似地,类也是按完整限定名称来封存的,因此在解封环境中也会应用相同的限制。 请注意类的代码或数据都不会被封存,因此在下面的示例中类属性 attr 不会在解封环境中被恢复:

 
 
 
 
  1. class Foo:
  2. attr = 'A class attribute'
  3. picklestring = pickle.dumps(Foo)

这些限制决定了为什么可封存的函数和类必须在一个模块的最高层级上定义。

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

封存类实例

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

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

 
 
 
 
  1. def save(obj):
  2. return (obj.__class__, obj.__dict__)
  3. def restore(cls, attributes):
  4. obj = cls.__new__(cls)
  5. obj.__dict__.update(attributes)
  6. return obj

类可以改变默认行为,只需定义以下一种或几种特殊方法:

object.__getnewargs_ex__()

对于使用第 2 版或更高版协议的 pickle,实现了 __getnewargs_ex__() 方法的类可以控制在解封时传给 __new__() 方法的参数。本方法必须返回一对 (args, kwargs) 用于构建对象,其中 args 是表示位置参数的 tuple,而 kwargs 是表示命名参数的 dict。它们会在解封时传递给 __new__() 方法。

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

在 3.6 版更改: __getnewargs_ex__() 现在可用于第 2 和第 3 版协议。

object.__getnewargs__()

这个方法与上一个 __getnewargs_ex__() 方法类似,但仅支持位置参数。它要求返回一个 tuple 类型的 args,用于解封时传递给 __new__() 方法。

如果定义了 __getnewargs_ex__(),那么 __getnewargs__() 就不会被调用。

在 3.6 版更改: 在 Python 3.6 前,第 2、3 版协议会调用 __getnewargs__(),更高版本协议会调用 __getnewargs_ex__()。

object.__getstate__()

Classes can further influence how their instances are pickled by overriding the method __getstate__(). It is called and the returned object is pickled as the contents for the instance, instead of a default state. There are several cases:

在 3.11 版更改: Added the default implementation of the __getstate__() method in the object class.

object.__setstate__(state)

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

备注

如果 __getstate__() 返回 False,那么在解封时就不会调用 __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。每个元素代表的意义如下:

object.__reduce_ex__(protocol)

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

持久化外部对象

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

pickle 模块不提供对持久化 ID 的解析工作,它将解析工作分配给用户定义的方法,分别是 pickler 中的 persistent_id() 方法和 unpickler 中的 persistent_load() 方法。

要通过持久化 ID 将外部对象封存,必须在 pickler 中实现 persistent_id() 方法,该方法接受需要被封存的对象作为参数,返回一个 None 或返回该对象的持久化 ID。如果返回 None,该对象会被按照默认方式封存为数据流。如果返回字符串形式的持久化 ID,则会封存这个字符串并加上一个标记,这样 unpickler 才能将其识别为持久化 ID。

要解封外部对象,Unpickler 必须实现 persistent_load() 方法,接受一个持久化 ID 对象作为参数并返回一个引用的对象。

下面是一个全面的例子,展示了如何使用持久化 ID 来封存外部对象。

 
 
 
 
  1. # Simple example presenting how persistent ID can be used to pickle
  2. # external objects by reference.
  3. import pickle
  4. import sqlite3
  5. from collections import namedtuple
  6. # Simple class representing a record in our database.
  7. MemoRecord = namedtuple("MemoRecord", "key, task")
  8. class DBPickler(pickle.Pickler):
  9. def persistent_id(self, obj):
  10. # Instead of pickling MemoRecord as a regular class instance, we emit a
  11. # persistent ID.
  12. if isinstance(obj, MemoRecord):
  13. # Here, our persistent ID is simply a tuple, containing a tag and a
  14. # key, which refers to a specific record in the database.
  15. return ("MemoRecord", obj.key)
  16. else:
  17. # If obj does not have a persistent ID, return None. This means obj
  18. # needs to be pickled as usual.
  19. return None
  20. class DBUnpickler(pickle.Unpickler):
  21. def __init__(self, file, connection):
  22. super().__init__(file)
  23. self.connection = connection
  24. def persistent_load(self, pid):
  25. # This method is invoked whenever a persistent ID is encountered.
  26. # Here, pid is the tuple returned by DBPickler.
  27. cursor = self.connection.cursor()
  28. type_tag, key_id = pid
  29. if type_tag == "MemoRecord":
  30. # Fetch the referenced record from the database and return it.
  31. cursor.execute("SELECT * FROM memos WHERE key=?", (str(key_id),))
  32. key, task = cursor.fetchone()
  33. return MemoRecord(key, task)
  34. else:
  35. # Always raises an error if you cannot return the correct object.
  36. # Otherwise, the unpickler will think None is the object referenced
  37. # by the persistent ID.
  38. raise pickle.UnpicklingError("unsupported persistent object")
  39. def main():
  40. import io
  41. import pprint
  42. # Initialize and populate our database.
  43. conn = sqlite3.connect(":memory:")
  44. cursor = conn.cursor()
  45. cursor.execute("CREATE TABLE memos(key INTEGER PRIMARY KEY, task TEXT)")
  46. tasks = (
  47. 'give food to fish',
  48. 'prepare group meeting',
  49. 'fight with a zebra',
  50. )
  51. for task in tasks:
  52. cursor.execute("INSERT INTO memos VALUES(NULL, ?)", (task,))
  53. # Fetch the records to be pickled.
  54. cursor.execute("SELECT * FROM memos")
  55. memos = [MemoRecord(key, task) for key, task in cursor]
  56. # Save the records using our custom DBPickler.
  57. file = io.BytesIO()
  58. DBPickler(file).dump(memos)
  59. print("Pickled records:")
  60. pprint.pprint(memos)
  61. # Update a record, just for good measure.
  62. cursor.execute("UPDATE memos SET task='learn italian' WHERE key=1")
  63. # Load the records from the pickle data stream.
  64. file.seek(0)
  65. memos = DBUnpickler(file, conn).load()
  66. print("Unpickled records:")
  67. pprint.pprint(memos)
  68. if __name__ == '__main__':
  69. main()

Dispatch 表

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

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

例如

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

创建了一个带有自有 dispatch 表的 pickle.Pickler 实例,它可以对 SomeClass 类进行特殊处理。另外,下列代码

 
 
 
 
  1. class MyPickler(pickle.Pickler):
  2. dispatch_table = copyreg.dispatch_table.copy()
  3. dispatch_table[SomeClass] = reduce_SomeClass
  4. f = io.BytesIO()
  5. p = MyPickler(f)

完成同样的操作,但所有 MyPickler 的实例都会共享一个私有分发表。 另一方面,代码

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

会修改由 copyreg 模块的所有用户共享的全局分发表。

处理有状态的对象

下面的示例展示了如何修改类在封存时的行为。其中 TextReader 类打开了一个文本文件,每次调用其 readline() 方法则返回行号和该行的字符。 在封存这个 TextReader 的实例时,除了 文件对象,其他属性都会被保存。 当解封实例时,需要重新打开文件,然后从上次的位置开始继续读取。实现这些功能需要实现 __setstate__()__getstate__() 方法。

 
 
 
 
  1. class TextReader:
  2. """Print and number lines in a text file."""
  3. def __init__(self, filename):
  4. self.filename = filename
  5. self.file = open(filename)
  6. self.lineno = 0
  7. def readline(self):
  8. self.lineno += 1
  9. line = self.file.readline()
  10. if not line:
  11. return None
  12. if line.endswith('\n'):
  13. line = line[:-1]
  14. return "%i: %s" % (self.lineno, line)
  15. def __getstate__(self):
  16. # Copy the object's state from self.__dict__ which contains
  17. # all our instance attributes. Always use the dict.copy()
  18. # method to avoid modifying the original state.
  19. state = self.__dict__.copy()
  20. # Remove the unpicklable entries.
  21. del state['file']
  22. return state
  23. def __setstate__(self, state):
  24. # Restore instance attributes (i.e., filename and lineno).
  25. self.__dict__.update(state)
  26. # Restore the previously opened file's state. To do so, we need to
  27. # reopen it and read from it until the line count is restored.
  28. file = open(self.filename)
  29. for _ in range(self.lineno):
  30. file.readline()
  31. # Finally, save the file.
  32. self.file = file

使用方法如下所示:

 
 
 
 
  1. >>> reader = TextReader("hello.txt")
  2. >>> reader.readline()
  3. '1: Hello world!'
  4. >>> reader.readline()
  5. '2: I am line number two.'
  6. >>> new_reader = pickle.loads(pickle.dumps(reader))
  7. >>> new_reader.readline()
  8. '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 的具体实例。

以下是一个简单的例子,其中我们允许封存并重新构建一个给定的类:

 
 
 
 
  1. import io
  2. import pickle
  3. class MyClass:
  4. my_attribute = 1
  5. class MyPickler(pickle.Pickler):
  6. def reducer_override(self, obj):
  7. """Custom reducer for MyClass."""
  8. if getattr(obj, "__name__", None) == "MyClass":
  9. return type, (obj.__name__, obj.__bases__,
  10. {'my_attribute': obj.my_attribute})
  11. else:
  12. # For any other object, fallback to usual reduction
  13. return NotImplemented
  14. f = io.BytesIO()
  15. p = MyPickler(f)
  16. p.dump(MyClass)
  17. del MyClass
  18. unpickled_class = pickle.loads(f.getvalue())
  19. assert isinstance(unpickled_class, type)
  20. assert unpickled_class.__name__ == "MyClass"
  21. assert unpickled_class.my_attribute == 1

外部缓冲区

3.8 新版功能.

在某些场景中,pickle 模块会被用来传输海量的数据。 因此,最小化内存复制次数以保证性能和节省资源是很重要的。 但是 pickle 模块的正常运作会将图类对象结构转换为字节序列流,因此在本质上就要从封存流中来回复制数据。

如果 provider (待传输对象类型的实现) 和 consumer (通信系统的实现) 都支持 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 对象。

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

示例

下面是一个小例子,在其中我们实现了一个 bytearray 的子类,能够用于外部缓冲区封存:

 
 
 
 
  1. class ZeroCopyByteArray(bytearray):
  2. def __reduce_ex__(self, protocol):
  3. if protocol >= 5:
  4. return type(self)._reconstruct, (PickleBuffer(self),), None
  5. else:
  6. # PickleBuffer is forbidden with pickle protocols <= 4.
  7. return type(self)._reconstruct, (bytearray(self),)
  8. @classmethod
  9. def _reconstruct(cls, obj):
  10. with memoryview(obj) as m:
  11. # Get a handle over the original buffer object
  12. obj = m.obj
  13. if type(obj) is cls:
  14. # Original buffer object is a ZeroCopyByteArray, return it
  15. # as-is.
  16. return obj
  17. else:
  18. return cls(obj)

重构造器 (_reconstruct 类方法) 会在缓冲区的提供对象具有正确类型时返回该对象。 在此小示例中这是模拟零拷贝行为的便捷方式。

在使用方,我们可以按通常方式封存那些对象,它们在反序列化时将提供原始对象的一个副本:

 
 
 
 
  1. b = ZeroCopyByteArray(b"abc")
  2. data = pickle.dumps(b, protocol=5)
  3. new_b = pickle.loads(data)
  4. print(b == new_b) # True
  5. print(b is new_b) # False: a copy was made

但是如果我们传入 buffer_callback 然后在反序列化时给回累积的缓冲区,我们就能够取回原始对象:

 
 
 
 
  1. b = ZeroCopyByteArray(b"abc")
  2. buffers = []
  3. data = pickle.dumps(b, protocol=5, buffer_callback=buffers.append)
  4. new_b = pickle.loads(data, buffers=buffers)
  5. print(b == new_b) # True
  6. print(b is new_b) # True: no copy was made

这个例子受限于 bytearray 会自行分配内存这一事实:你无法基于另一个对象的内存创建 bytearray 的实例。 但是,第三方数据类型例如 NumPy 数组则没有这种限制,允许在单独进程或系统间传输时使用零拷贝的封存(或是尽可能少地拷贝) 。

参见

PEP 574 — 带有外部数据缓冲区的 pickle 协议 5

限制全局变量

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

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

在这个例子里,解封器导入 os.system() 函数然后应用字符串参数 “echo hello world”。 虽然这个例子不具攻击性,但是不难想象别人能够通过此方式对你的系统造成损害。

出于这样的理由,你可能会希望通过定制 Unpickler.find_class() 来控制要解封的对象。 与其名称所提示的不同,Unpickler.find_class() 会在执行对任何全局对象(例如一个类或一个函数)的请求时被调用。 因此可以完全禁止全局对象或是将它们限制在一个安全的子集中。

下面的例子是一个解封器,它只允许某一些安全的来自 builtins 模块的类被加载:

 
 
 
 
  1. import builtins
  2. import io
  3. import pickle
  4. safe_builtins = {
  5. 'range',
  6. 'complex',
  7. 'set',
  8. 'frozenset',
  9. 'slice',
  10. }
  11. class RestrictedUnpickler(pickle.Unpickler):<
    文章名称:创新互联Python教程:pickle—-Python对象序列化
    浏览路径:http://cdbrznjsb.com/article/dpecice.html
  • 网站建设专属方案

  • 网站定制化设计

  • 7X24小时服务

  • N对管家服务

让你的专属顾问为你服务