跳转到主要内容

Pickle的Python 2/3兼容层

项目描述

pickle-compat

tests

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不希望处理“字符串”,因为这个名字含糊不清,它更喜欢处理BINBYTESBINUNICODE。我将展示在协议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)

我们丢失了ab的属性。它们去哪里了?同样的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}

好的,我们不能使用ASCIIlatin1utf8作为编码,现在我们了解到我们也不能使用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 模块。

项目详情


下载文件

下载适合您平台的文件。如果您不确定选择哪个,请了解有关 安装包 的更多信息。

源分布

pickle-compat-2.1.1.tar.gz (19.5 kB 查看哈希)

上传时间

构建分布

pickle_compat-2.1.1-py2.py3-none-any.whl (11.7 kB 查看哈希)

上传时间 Python 2 Python 3

由以下支持

AWS AWS 云计算和安全赞助商 Datadog Datadog 监控 Fastly Fastly CDN Google Google 下载分析 Microsoft Microsoft PSF赞助商 Pingdom Pingdom 监控 Sentry Sentry 错误记录 StatusPage StatusPage 状态页面