为何Python 3无法读取Python 2的pickle数据并自动做版本转换?
Python 3 本身完全支持识别所有历史版本的pickle协议,出现这个解码错误和协议版本识别无关,本质是Python 2和Python 3的核心字符串类型模型存在不兼容的断层,不存在无歧义的默认转换规则,因此无法做到完全自动适配。
两个版本的字符串类型差异
- Python 2 存在两类字符串:
str是纯字节串,unicode才是Unicode文本类型。代码中不带u前缀定义的字符串,本质是按照源文件声明的编码(你这里是utf-8)编码后的字节序列,本身不携带编码信息。 - Python 3 中
str是默认的Unicode文本类型,对应Python 2的unicode;单独的bytes类型才对应Python 2的旧str字节串类型。
报错触发逻辑
当Python 3的pickle模块读取到Python 2序列化的str(字节串)对象时,默认行为是尝试用ascii编码将其解码为Python 3的str文本类型。你测试用例里的键是带重音的拉丁字母,utf-8编码后的字节值超过了ascii编码支持的0-127范围,因此直接抛出UnicodeDecodeError。
为什么不做自动适配
不存在通用的、不会出错的自动转换逻辑:Python 2的str里存的可能是任意编码的文本,也可能是完全不需要解码的二进制数据(比如图片字节、加密流、结构化二进制内容)。pickle模块无法预判字节内容的实际用途,也不可能猜对所有场景下的编码格式,擅自做转换导致的静默数据错误,比直接抛出错误的危害大得多,因此官方选择把解码规则的决定权交给用户。
读取时明确给pickle.load传入对应编码参数即可,你的场景中写入时用的是utf-8编码的文本,直接指定encoding='utf-8'就能正常读取:
# Python 3 兼容读取Python2生成pickle的代码 import pickle with open('test.pickle', 'rb') as f: data = pickle.load(f, encoding='utf-8') print(data)
如果不确定旧pickle里的字节串内容,优先用encoding='latin1':这是单字节一一映射的编码,能把0-255范围内的所有字节无错映射到Unicode码点,不会抛出解码错误,读取完成后可以再自行区分处理二进制内容和文本内容。
另外补充一个示例代码里的隐患:Python 2写入pickle时应该用'wb'二进制模式打开文件,用文本模式'w'写入会在Windows等平台触发自动换行符转换,大概率会损坏pickle文件结构。
“高版本一定能兼容读取低版本数据”的前提,是低版本序列化的数据语义在高版本中是明确、无歧义的。Python 2到3是核心类型层面的不兼容升级,旧版str类型在新版中没有唯一对应的默认映射关系:如果默认转成bytes,所有读取文本内容的场景都要手动解码,使用成本极高;如果默认按某一种编码转成str,遇到编码不匹配或者二进制内容时会出现静默的数据损坏。当前的默认报错逻辑,是权衡使用成本和数据安全性之后的选择。
内容的提问来源于stack exchange,提问作者G G

