Pickle的Python 2/3兼容层
项目描述
pickle-compat
Pickle的Python 2/3兼容层
TL;DR
要使您的pickle在Python版本之间向前和向后兼容,请使用此方法
pip install pickle-compat
然后使用此方法对您的pickle库进行猴子补丁
import pickle_compat
pickle_compat.patch()
从这一点起,您可以安全地假设在Python 2中使用pickle.dumps()
序列化的内容可以在Python 3中使用pickle.loads()
转换回实际对象,反之亦然。但是,请注意,它与cPickle、future.moves.pickle或six.moves.cPickle不兼容,您需要使用普通的“import pickle”。
如果您想撤销补丁,请使用
pickle_compat.unpatch()
问题声明
您始终知道pickle的不安全性、调试困难,以及如果您决定更新版本,向后不兼容的问题可能会让您受到打击。您也听说过,您永远不应该在多语言环境中使用pickle,因为它是Python特定的。
您已知一切,但您认为这“足够好”,适用于您的案例。您一直在使用单体应用,pickle提供了一种开箱即用的序列化机制,适用于您从Python代码创建的任何内容。
然而,到了迁移到Python 3的时候。您焦急万分,尽可能地推迟了您的大型遗留应用程序的迁移,但再也无法推迟了。这时,您意识到Python 2和Python 3并不是同一种语言的两个版本,而实际上是两种不同的语言,恰好共享一些代码结构。
好的,现在您突然面临了一个多语言环境,您需要从Python 3中的代码读取由Python 2序列化的pickle内容。如果您正在逐步迁移,反之亦然。
最初的挫折
只有在最简单的情况下,一切才能顺利工作。
$ python2 -c 'import pickle; print pickle.dumps("Hello world")' | python3 -c 'import pickle, sys; print(repr(pickle.load(sys.stdin.buffer)))'
'Hello world'
突然间,一些最意想不到的地方开始出现问题。例如,Python 3无法反序列化Python 2的datetime,抛出了Python开发者最可怕的问题之一,UnicodeDecodeError。
$ python2 -c 'import pickle, datetime; print pickle.dumps(datetime.datetime.utcnow())' | python3 -c 'import pickle, sys; print(repr(pickle.load(sys.stdin.buffer)))'
Traceback (most recent call last):
File "<string>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 1: ordinal not in range(128)
让我们跟随兔子了解更多关于pickle的信息,足以使其在Python 2和Python 3中工作。在这个时候,我不确定如何使您从当前的状态平稳过渡到我希望我们达到的状态,所以我开始向您抛出一些随机的事实,希望它们能在您的脑海中形成一个或多或少一致的图像。
协议版本
Pickle有几个所谓的“协议”,或文件可以写入的格式。您可以在pickle.dumps()
中可选地定义协议版本。Python 2.7的默认格式是0(也称为ASCII格式),但它也可以读取和写入格式1和2。格式1和2不是ASCII安全的,但它们更紧凑,更快。
>>> pickle.dumps("hello")
"S'hello'\np0\n."
>>> pickle.dumps("hello", protocol=1)
'U\x05helloq\x00.'
>>> pickle.dumps("hello", protocol=2)
'\x80\x02U\x05helloq\x00.'
在Python 3中,Guido引入了协议的新版本,故意使其与Python 2.7不兼容。查看提交。围绕DEFAULT_PROTOCOL
常量的注释警告说:“我们故意编写一个Python 2.x无法读取的协议;这方面有很多问题。”
我们主要想说的是,如果我们想要有前后兼容的代码,我们只能使用Python 2和Python 3都能理解的协议:从0到2inclusive。
Pickle格式和pickletools
pickletools模块将自己称为pickle模块的“可执行文档”。我强烈建议我们打开源代码,阅读一个广泛的介绍,从“pickle是一种为虚拟pickle机器编写的程序。”开始。pickletools的另一个有用功能是它提供了一个可读的pickle堆栈表示。
$ python2
>>> import pickle, pickletools
>>> pickletools.dis(pickle.dumps("hello"))
0: S STRING 'hello'
9: p PUT 0
12: . STOP
highest protocol among opcodes = 0
这里的重点是pickle中的数据以“操作码 - 数据”的格式表示,其中操作码大致决定了后续元素的类型。操作码列表相当广泛,并且一直在增长。您可以在这里找到它们。
字符串和字节
让我们找出文本和字节在Python 2和Python 3中的表示方式,以及它们之间的差异。我们将使用pickle版本2进行比较。Python 2将字符串和字节编码为BINSTRING
,将Unicode对象编码为BINUNICODE
,这并不令人惊讶。
$ python2
>>> import pickle, pickletools
>>> pickletools.dis(pickle.dumps("foo", protocol=2))
0: \x80 PROTO 2
2: U SHORT_BINSTRING 'foo'
7: q BINPUT 0
9: . STOP
highest protocol among opcodes = 2
>>> pickletools.dis(pickle.dumps(b"foo", protocol=2))
0: \x80 PROTO 2
2: U SHORT_BINSTRING 'foo'
7: q BINPUT 0
9: . STOP
highest protocol among opcodes = 2
>>> pickletools.dis(pickle.dumps(u"foo", protocol=2))
0: \x80 PROTO 2
2: X BINUNICODE u'foo'
10: q BINPUT 0
12: . STOP
highest protocol among opcodes = 2
相反,Python 3不希望处理“字符串”,因为这个名字含糊不清,它更喜欢处理BINBYTES
和BINUNICODE
。我将展示在协议3中的编码方式,但这并不意味着与Python 2兼容。
$ python3
>>> import pickle, pickletools
>>> pickletools.dis(pickle.dumps(b"foo", protocol=3))
0: \x80 PROTO 3
2: C SHORT_BINBYTES b'foo'
7: q BINPUT 0
9: . STOP
highest protocol among opcodes = 3
>>> pickletools.dis(pickle.dumps(u"foo", protocol=3))
0: \x80 PROTO 3
2: X BINUNICODE 'foo'
10: q BINPUT 0
12: . STOP
highest protocol among opcodes = 2
下面是两个问题
- Python 3在协议2中如何编码字节?请注意,第二个协议对
BINBYTES
一无所知? - 如何在Python 3中解码
BINSTRING
类型,假设它是一个Python 2类型并且是模糊的?
回答第一个问题很简单。pickle引入了一个向后兼容的技巧。
$ python3
>>> pickletools.dis(pickle.dumps(b'foo', protocol=2))
0: \x80 PROTO 2
2: c GLOBAL '_codecs encode'
18: q BINPUT 0
20: X BINUNICODE 'foo'
28: q BINPUT 1
30: X BINUNICODE 'latin1'
41: q BINPUT 2
43: \x86 TUPLE2
44: q BINPUT 3
46: R REDUCE
47: q BINPUT 4
49: . STOP
highest protocol among opcodes = 2
转换回Python时,它将字节序列保存到Unicode对象中,将其放入栈中,并告诉unpickler执行以下命令
import _codecs
_codecs.encode(u"foo", "latin1")
一个侧注。我不知道,但显然,你可以安全地将任何字节序列转换为Unicode再转换回来。
$ python3
>>> import os
>>> s = os.urandom(100000)
>>> s == s.decode('latin1').encode('latin1')
True
这也适用于Python 2,所以我们不必太关心向后兼容性。
现在,Python 3是如何解码BINSTRING
操作码的?从第一个例子中,我们可以看到在Python 2中字符串现在是Python 3中的Unicode对象。换句话说,pickle试图将字节转换为Unicode。
$ python2 -c 'import pickle; print pickle.dumps("Hello world")' | python3 -c 'import pickle, sys; print(repr(pickle.load(sys.stdin.buffer)))'
'Hello world'
此时,你可能会问自己它使用什么编码?幸运的是,答案就在文档中。Python 3引入了一个名为“encoding”的参数,默认值为ASCII。
编码和错误告诉pickle如何解码由Python 2序列化的8位字符串实例;这些默认为‘ASCII’和‘strict’。编码可以是‘bytes’,以将这些8位字符串实例作为字节对象读取。使用encoding='latin1'是解冻Python 2序列化的NumPy数组和datetime、date和time实例所必需的。
如果你想知道datetime有什么问题,这里是如何在Python 2中显示其输出的。
$ python2
>>> import pickle, pickletools, datetime
>>> pickletools.dis(pickle.dumps(datetime.datetime.utcnow(), protocol=2))
0: \x80 PROTO 2
2: c GLOBAL 'datetime datetime'
21: q BINPUT 0
23: U SHORT_BINSTRING '\x07\xe4\x05\x1a\x0f\x01\x16\x00\x96\x10'
35: q BINPUT 1
37: \x85 TUPLE1
38: q BINPUT 2
40: R REDUCE
41: q BINPUT 3
43: . STOP
highest protocol among opcodes = 2
这里是我遇到的一个惊喜:datetime构造函数可以接受一个字节序列来初始化其内部状态,而pickle就利用了这一点。
>>> import datetime
>>> datetime.datetime(b'\x07\xe4\x05\x1a\x0f\x01\x16\x00\x96\x10')
datetime.datetime(2020, 5, 26, 15, 1, 22, 38416)
将编码设置为"latin1"似乎可以工作。
python2 -c 'import pickle, datetime; print pickle.dumps(datetime.datetime.utcnow())' | python3 -c 'import pickle, sys; print(repr(pickle.load(sys.stdin.buffer, encoding="latin1")))'
datetime.datetime(2020, 5, 26, 15, 19, 6, 275120)
主要收获是Python 2中的字符串在Python 3中转换为Unicode对象,并且你可以控制编码。
Python 2中的非拉丁字符
如果你有非ASCII内容,表示为旧字符串而不是Unicode对象,那会怎样?如果你在Python 2中序列化它,然后在Python 3中解冻它,你就会遇到麻烦。
字节字符串没有任何关于编码的信息。在Python 2中,你可能会隐式地假设它是UTF-8,但是当你用Unpickle将其转换回Python 3时,它看起来像是用latin1编码的。
python2 -c 'import pickle; print pickle.dumps("©")' | python3 -c 'import pickle, sys; print(repr(pickle.load(sys.stdin.buffer, encoding="latin1")))'
'©'
为了解决这个问题,你需要使用UTF-8,这在这种情况下是有效的。
python2 -c 'import pickle; print pickle.dumps("©")' | python3 -c 'import pickle, sys; print(repr(pickle.load(sys.stdin.buffer, encoding="utf8")))'
'©'
不幸的是,对于表示无效UTF-8序列的日期时间和其他二进制字符串,它将不起作用。
使用"bytes"编码解冻
好吧,我们离胜利如此之近,又回到了起点。我们接下来要做什么?幸运的是,有一个官方的逃生舱口,即"bytes"编码。这种编码看起来正是我们需要的。它不会试图欺骗你并将字节转换为看起来像字符串的东西。相反,它返回字节作为字节对象。甚至比"latin1"更好!
python2 -c 'import pickle; print pickle.dumps("©")' | python3 -c 'import pickle, sys; print(repr(pickle.load(sys.stdin.buffer, encoding="bytes")))'
b'\xc2\xa9'
日期时间对象也有效。这是胜利吗?别那么快。
使用"bytes"编码解冻。字符串常量
我从不在扮演“字符串常量”角色的字符串常量前加u""
前缀。这很丑陋且多余,在大多数情况下,当我的字符串不包含任何非ASCII符号时,迁移工作得很好。
但是,当pickle介入时,情况并非如此。例如,考虑一个接受两个参数和一个操作名称作为字符串的函数
def apply_operation(a, b, op):
if op == "ADD":
return a + b
elif op == "SUB":
return a - b
else:
raise ValueError("Unknown operation")
在其他地方,我让op
通过pickle-unpickle管道,这样Python 2会将其转换为二进制字符串,而Python 3则会原样反序列化。在我的情况下,这可能是缓存库或队列处理器。现在,我将我的op
作为二进制对象传递给函数,因为"ADD" != b"ADD"
,它总是会因为“未知操作”异常而失败。
字节与Unicode问题最常见的例子如下。情况一
>>> "foo" == b"foo"
False
情况二
>>> {"key": "value"}[b"key"]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: b'key'
一个解决方案是使所有字符串常量都表现得像Unicode对象。你可以将它们显式地从""
转换为u""
,或者添加from __future__ import unicode_literals
。这两种解决方案看起来都不太优雅。幸运的是,当你最终迁移到Python 3时,你可以删除它们,而且可以自动这样做。我更喜欢“未来”解决方案,因为它生成的差异更小。
futurize --stage1 --unicode-literals --write --nobackups path/to/code
使用“bytes”编码进行反序列化。具有属性的对象
这绝对不是最坏的情况。为了让事情更加复杂,让我们尝试序列化foo.foo
。
# file: foo.py
class Foo(object):
a = 'UNSET'
b = 'UNSET'
def __init__(self):
self.a = 1
self.b = 2
def __repr__(self):
return 'Foo(%s, %s)' % (self.a, self.b)
foo = Foo()
只要我们使用默认设置,我们就没有问题。
$ python2 -c 'import pickle, foo; print pickle.dumps(foo.foo)' | python3 -c 'import pickle, sys; print(repr(pickle.load(sys.stdin.buffer)))'
Foo(1, 2)
但如果我们传递“bytes”作为参数,突然之间,某些东西就出问题了。
python2 -c 'import pickle, foo; print pickle.dumps(foo.foo)' | python3 -c 'import pickle, sys; print(repr(pickle.load(sys.stdin.buffer, encoding="bytes")))'
Foo(UNSET, UNSET)
我们丢失了a
和b
的属性。它们去哪里了?同样的pickletool.dis()
帮助我们找到答案
$ python2
>>> import pickle, pickletools, foo
>>> pickletools.dis(pickle.dumps(foo.foo, protocol=2))
0: \x80 PROTO 2
2: c GLOBAL 'foo Foo'
11: q BINPUT 0
13: ) EMPTY_TUPLE
14: \x81 NEWOBJ
15: q BINPUT 1
17: } EMPTY_DICT
18: q BINPUT 2
20: ( MARK
21: U SHORT_BINSTRING 'a'
24: q BINPUT 3
26: K BININT1 1
28: U SHORT_BINSTRING 'b'
31: q BINPUT 4
33: K BININT1 2
35: u SETITEMS (MARK at 20)
36: b BUILD
37: . STOP
highest protocol among opcodes = 2
pickle加载器不调用__init__
。相反,它创建了一个新的空“dummy”对象,该对象的类是Foo
,并通过更新__dict__
来填充其状态。如果这是Python,我们可以这样写
obj = object.__new__(foo.Foo)
obj.__dict__ = {"a": 1, "b": 2}
我想现在你应该明白出了什么问题。因为bytes
编码,我们没有将b"a"和b"b"转换为它们的"python3-string"表示形式。你可以向对象的字典中放入任何东西,但只有键是字符串的才会被表示为“正确的对象属性”。
下面的命令显示了对象的__dict__
的内容,并证明了我们是正确的?
python2 -c 'import pickle, foo; print pickle.dumps(foo.foo)' | python3 -c 'import pickle, sys; print(pickle.load(sys.stdin.buffer, encoding="bytes").__dict__)'
{b'a': 1, b'b': 2}
好的,我们不能使用ASCII
、latin1
、utf8
作为编码,现在我们了解到我们也不能使用bytes
?这看起来是个死胡同。或者你可以采取最后的手段,即不道德的猴子修补。pickle-compat的前一个版本使用了这种方法,但我们最终决定放弃它,转而使用“latin1”,因为存在太多边缘情况。
回到使用“latin1”编码的反序列化。小心处理非ASCII字符串
所以,正如我们所学的,唯一实用的反序列化选项是将Python 2的str自动解码为Python 3的str,使用“latin1”作为编码。然而,正如我们之前讨论的,我们需要非常小心处理UTF-8隐式编码的字节字符串。我只会提供一些例子,在这些例子中你可能会意外遇到它们。无论如何,你需要修复以下所有内容,无论你是否计划处理pickle情况。
代码
这返回str
# coding: utf-8
copy = "©"
使用这个替代方案
# coding: utf-8
from __future__ import unicode_literals
copy = "©"
文件
这返回一个str
open('test.txt').read()
使用返回unicode并正确在Python 2和Python 3中工作的变体
import io
io.open('test.txt', 'rt', encoding='utf8').read()
Redis
这返回一个str
redis.Redis().get("foo")
使用decode_responses
redis.Redis(decode_responses=True).get("foo")
CSV读取器
这返回str对象
>>> import csv
>>> list(csv.reader(open("foo.csv")))
[['a', 'b'], ['c', 'd']]
安装backports.csv并在文本模式下打开文件
>>> from backports import csv
>>> import io
>>> list(csv.reader(io.open("foo.csv", "rt", encoding="utf8")))
[[u'a', u'b'], [u'c', u'd']]
Requests
这返回一个str
requests.get("https://example.com").content
使用返回unicode的变体
requests.get("https://example.com").text
旧式类
我们几乎完成了,除了最后一件事:旧式类。正如你所知,在Python 3中,一切都是object
的子类,而在Python 2中,除非你明确从它继承,否则顶级类将是“type”。这被认为是过时的,但它仍然被标准库的不同部分使用,等待着在最意想不到的时刻破坏你的生活。
这次我们讨论向前兼容性,并确保任何在Python 3中序列化的对象都可以在Python 2中成功反序列化。
让我们以一个在Python 2中是旧式类的对象为例。
python3 -c 'import pickle, smtplib, sys; sys.stdout.buffer.write(pickle.dumps(smtplib.SMTP(), protocol=2))' | python2 -c 'import pickle, sys; print pickle.load(sys.stdin)'
Traceback (most recent call last):
File "<string>", line 1, in <module>
File "2.7.15/lib/python2.7/pickle.py", line 1384, in load
return Unpickler(file).load()
File "2.7.15/lib/python2.7/pickle.py", line 864, in load
dispatch[key](self)
File "2.7.15/lib/python2.7/pickle.py", line 1089, in load_newobj
obj = cls.__new__(cls, *args)
AttributeError: class SMTP has no attribute '__new__'
方法与旧方法类似:找出反序列化器如何加载新对象,然后对其进行修改以查看类是否过时。Python 2 的实现位于此处。
注意,协议版本 0 不包含 NEWOBJ 操作码,使用一组变通方法使其工作,因此这种方法仅适用于协议版本 2。
cPickle、future 和 six 移动
以下是一则警告。修补器不会修复 Python 2 的 cPickle 和 Python 3 的 _pickle。后者是 Python 3 的 pickle 导入的未记录模块,如果可能的话。
在 Doist,我们通过在所有地方导入 "pickle" 来解决该问题的方法。它在 Python 2 上运行较慢,但这仅作为加快迁移速度的额外激励。您可以使用 "future" 包中的 futurize 自动执行,它将所有 import cPickle
的出现转换为 import pickle.
如果您选择不同的迁移策略,使用 "moves",这可能会变得繁琐,因为您可能会无意中导入 cPickle。更具体地说,这将在幕后导入 cPickle 实现
from future.moves import pickle
这同样适用于此
from six.moves import cPickle
主要教训是,如果您使用 cPickle、future.moves.pickle 或 six.moves.cPickle,此修补器将不会像预期那样工作。
综合以上内容
我们学到了什么
- 对于 Python 2 和 Python 3,协议的默认版本必须为 2。
- 我们必须在 Python 3 的 pickle 中使用 "latin1" 编码。
- 我们必须对纯字符串保持警惕,它们代表非 ASCII 对象。
- 我们必须修补 Python 2 的 Unpickler,以正确反序列化旧式类的实例。
此外,我们还了解了一些 pickle 的内部机制,并学习了如何使用 pickletools。最后,我们使用 pickle_compat
库将所有内容封装起来,该库会猴子补丁标准 pickle 模块。
项目详情
下载文件
下载适合您平台的文件。如果您不确定选择哪个,请了解有关 安装包 的更多信息。