PEP 3154 – Pickle 协议版本 4
- 作者:
- Antoine Pitrou <solipsis at pitrou.net>
- 状态:
- 最终
- 类型:
- 标准跟踪
- 创建:
- 2011-08-11
- Python 版本:
- 3.4
- 发布历史:
- 2011-08-12
- 决议:
- Python-Dev 消息
摘要
使用 pickle 模块序列化的数据必须在不同 Python 版本之间可移植。它还应支持最新的语言功能以及特定于实现的功能。因此,pickle 模块了解几个协议(当前编号从 0 到 3),每个协议都出现在不同的 Python 版本中。使用低编号的协议版本允许与旧的 Python 版本交换数据,而使用高编号的协议版本允许访问更新的功能,有时还可以更有效地利用资源(序列化和反序列化所需的 CPU 时间,以及数据传输所需的磁盘大小/网络带宽)。
基本原理
最新的当前协议,巧合地称为协议 3,出现在 Python 3.0 中,并支持语言中的新不兼容功能(主要是默认情况下使用 Unicode 字符串和新的 bytes 对象)。当时没有机会以其他方式改进协议。
此 PEP 旨在促进新 pickle 协议版本中的一些增量改进。使用 PEP 过程是为了尽可能多地收集改进,因为引入新的 pickle 协议应该是一种罕见的事件。
拟议更改
框架
传统上,当从流中反序列化对象时(通过调用 load()
而不是 loads()
),可能会对类文件对象发出许多小的 read()
调用,这会对性能产生巨大的影响。
相比之下,协议 4 具有二进制框架。因此,pickle 的总体结构如下
+------+------+
| 0x80 | 0x04 | protocol header (2 bytes)
+------+------+
| OP | FRAME opcode (1 byte)
+------+------+-----------+
| MM MM MM MM MM MM MM MM | frame size (8 bytes, little-endian)
+------+------------------+
| .... | first frame contents (M bytes)
+------+
| OP | FRAME opcode (1 byte)
+------+------+-----------+
| NN NN NN NN NN NN NN NN | frame size (8 bytes, little-endian)
+------+------------------+
| .... | second frame contents (N bytes)
+------+
etc.
为了保持实现的简单性,禁止 pickle 操作码跨越帧边界。pickler 会注意不要生成此类 pickle,而 unpickler 会拒绝它们。此外,没有“最后一帧”标记。最后一帧只是以 STOP 操作码结束的帧。
编写良好的 C 实现不需要为框架层进行额外的内存复制,从而保留了总体(反)序列化的效率。
注意
pickler 如何将 pickle 流划分为帧是一个实现细节。例如,当帧达到约 64 KiB 时立即“关闭”帧对于性能和 pickle 大小的开销来说都是一个合理的选择。
所有操作码的二进制编码
GLOBAL 操作码仍在协议 3 中使用,它使用 pickle 协议的所谓“文本”模式,这涉及在 pickle 流中查找换行符。这也使二进制框架的实现变得复杂。
协议 4 禁止使用 GLOBAL 操作码,并将其替换为 STACK_GLOBAL,这是一个从堆栈获取其操作数的新操作码。
序列化更多“可查找”的对象
默认情况下,pickle 只能序列化模块全局函数和类。支持其他类型的对象,例如未绑定方法 [4],是一个常见的请求。实际上,对其中一些对象的第三方支持,例如绑定方法,是在 multiprocessing 模块中实现的 [5]。
来自 PEP 3155 的 __qualname__
属性使得可以通过名称查找更多对象成为可能。使 STACK_GLOBAL 操作码接受点分隔名称将允许标准 pickle 实现支持所有这些类型的对象。
用于大型对象的 64 位操作码
当前协议版本将各种内置类型的对象大小(str、bytes)导出为 32 位整数。这禁止对大型数据进行序列化 [1]。需要新的操作码来支持非常大的 bytes 和 str 对象。
集合和冻结集合的原生操作码
许多常见的内置类型(例如 str、bytes、dict、list、tuple)具有专用操作码来提高序列化和反序列化时的资源消耗;但是,集合和冻结集合没有。添加此类操作码将是一个明显的改进。此外,专用集合支持可以帮助消除当前无法对自引用集合进行 pickle 的情况 [2]。
使用关键字参数调用 __new__
目前,如果 __new__
强制使用仅关键字参数,则无法对类进行 pickle(或更确切地说,无法进行反序列化) [3]。需要一个新的特殊方法 (__getnewargs_ex__
) 和一个新的操作码 (NEWOBJ_EX)。如果存在 __getnewargs_ex__
方法,则必须返回一个包含两个元素的元组 (args, kwargs)
,其中第一个元素是位置参数的元组,第二个元素是类 __new__
方法的关键字参数的字典。
更好的字符串编码
短 str 对象当前的长度编码为 4 字节整数,这是一种浪费。具有 1 字节长度的特定操作码将使许多 pickle 变得更小。
更小的记忆化
PUT 操作码都需要一个显式索引来选择在记忆字典的哪个条目中记忆堆栈顶部的元素。但是,在实践中,这些数字是按顺序分配的。一个新的操作码 MEMOIZE 将改为将堆栈顶部的对象存储在等于记忆字典当前大小的索引处。这允许使用更短的 pickle,因为 PUT 操作码会为所有非原子数据类型发出。
新操作码摘要
这些反映了拟议实现的状态(主要归功于 Alexandre Vassalotti 的工作)
FRAME
: 引入一个新帧(后面跟着 8 字节帧大小和帧内容)。SHORT_BINUNICODE
: 推送一个使用 utf8 编码的 str 对象,该对象有一个 1 字节大小前缀(因此长度小于 256 字节)。BINUNICODE8
: 推送一个使用 utf8 编码的 str 对象,该对象有一个 8 字节大小前缀(对于长度超过 2**32 字节的字符串,因此无法使用BINUNICODE
进行序列化)。BINBYTES8
: 推送一个 bytes 对象,该对象有一个 8 字节大小前缀(对于长度超过 2**32 字节的 bytes 对象,因此无法使用BINBYTES
进行序列化)。EMPTY_SET
: 在堆栈上推送一个新的空集合对象。ADDITEMS
: 将堆栈顶部的项目添加到集合中(与EMPTY_SET
一起使用)。FROZENSET
: 从堆栈顶部的项目创建冻结集合对象,并将其推送到堆栈中。NEWOBJ_EX
: 获取堆栈顶部的三个项目cls
、args
和kwargs
,并推送到调用cls.__new__(*args, **kwargs)
的结果。STACK_GLOBAL
: 获取堆栈顶部的两个项目module_name
和qualname
,并推送到在名为module_name
的模块中查找点分隔的qualname
的结果。MEMOIZE
: 在记忆字典中使用等于记忆字典当前大小的索引存储堆栈顶部的对象。
其他想法
预取
Serhiy Storchaka 建议用一个特殊的 PREFETCH 操作码(带有 2 或 4 字节参数)来代替框架,以明确声明已知的 pickle 块。大型数据可以保存在这些块之外。一个简单的 unpickler 应该能够跳过 PREFETCH 操作码并仍然正确解码 pickle,但良好的错误处理将需要检查 PREFETCH 长度是否落在操作码边界上。
致谢
按字母顺序
参考文献
版权
此文档已进入公有领域。
来源: https://github.com/python/peps/blob/main/peps/pep-3154.rst
最后修改时间: 2023-09-09 17:39:29 GMT