PEP 3154 – Pickle 协议版本 4
- 作者:
- Antoine Pitrou <solipsis at pitrou.net>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2011年8月11日
- Python 版本:
- 3.4
- 发布历史:
- 2011年8月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 操作码禁止跨越帧边界。封存器会注意不产生此类 pickle,而解封器会拒绝它们。此外,没有“最后一帧”标记。最后一帧只是以 STOP 操作码结束的帧。
一个编写良好的 C 实现不需要额外的内存副本用于帧层,从而保持一般的(解)封存效率。
注意
封存器如何决定将 pickle 流划分为帧是一个实现细节。例如,“关闭”一个帧,只要它达到约 64 KiB,对于性能和 pickle 大小开销来说都是一个合理的选择。
所有操作码的二进制编码
协议 3 中仍然使用的 GLOBAL 操作码使用 pickle 协议的所谓“文本”模式,这涉及在 pickle 流中查找换行符。它还使二进制帧的实现复杂化。
协议 4 禁止使用 GLOBAL 操作码,并将其替换为 STACK_GLOBAL,这是一个从堆栈中获取其操作数的新操作码。
序列化更多“可查找”对象
默认情况下,pickle 只能序列化模块全局函数和类。支持其他类型的对象,例如未绑定方法 [4],是一个常见请求。实际上,多进程模块 [5] 中实现了对其中一些对象(例如绑定方法)的第三方支持。
来自 PEP 3155 的 __qualname__ 属性使得通过名称查找更多对象成为可能。让 STACK_GLOBAL 操作码接受点分隔的名称将允许标准 pickle 实现支持所有这些类型的对象。
用于大型对象的64位操作码
当前协议版本将各种内置类型(str、bytes)的对象大小导出为 32 位整数。这禁止序列化大型数据 [1]。需要新的操作码来支持非常大的 bytes 和 str 对象。
集合和不可变集合的原生操作码
许多常见的内置类型(如 str、bytes、dict、list、tuple)都有专门的操作码,以在序列化和反序列化它们时提高资源消耗;但是,集合和不可变集合没有。添加此类操作码将是一个明显的改进。此外,专门的集合支持可以帮助消除当前无法封存自引用集合的情况 [2]。
使用关键字参数调用 __new__
目前,要求使用仅关键字参数的 __new__ 的类无法被封存(或者更确切地说,无法被解封) [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 字符串对象(因此长度小于 256 字节)。
- BINUNICODE8: 推送一个以八字节大小前缀编码的 utf8 字符串对象(用于长度超过 2**32 字节的字符串,因此无法使用- BINUNICODE序列化)。
- BINBYTES8: 推送一个以八字节大小前缀编码的 bytes 对象(用于长度超过 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 块。大型数据可以在这些块之外进行 pickle。一个简单的解封器应该能够跳过 PREFETCH 操作码并仍然正确解码 pickle,但良好的错误处理将需要检查 PREFETCH 长度是否落在操作码边界上。
致谢
按字母顺序
参考资料
版权
本文档已置于公共领域。
来源: https://github.com/python/peps/blob/main/peps/pep-3154.rst
最后修改: 2025-02-01 08:59:27 GMT