为什么pickle的__getstate__方法返回值是要求__getstate__处理的实例本身?

12 投票
2 回答
6087 浏览
提问于 2025-04-16 13:20

我本来想问“如何对一个继承自dict并且定义了__slots__的类进行序列化”。然后我意识到下面的class B中的解决方案真的是让人费解,但它确实有效……

import pickle

class A(dict):
    __slots__ = ["porridge"]
    def __init__(self, porridge): self.porridge = porridge

class B(A):
    __slots__ = ["porridge"]
    def __getstate__(self):
        # Returning the very item being pickled in 'self'??
        return self, self.porridge 
    def __setstate__(self, state):
        print "__setstate__(%s) type(%s, %s)" % (state, type(state[0]), 
                                                type(state[1]))
        self.update(state[0])
        self.porridge = state[1]

这里是一些输出结果:

>>> saved = pickle.dumps(A(10))
TypeError: a class that defines __slots__ without defining __getstate__ cannot be pickled
>>> b = B('delicious')
>>> b['butter'] = 'yes please'
>>> loaded = pickle.loads(pickle.dumps(b))
__setstate__(({'butter': 'yes please'}, 'delicious')) type(<class '__main__.B'>, <type 'str'>)
>>> b
{'butter': 'yes please'}
>>> b.porridge
'delicious'

简单来说,pickle无法对一个定义了__slots__的类进行序列化,除非同时定义__getstate__。这就成了一个问题,因为如果这个类是从dict继承来的——那么在不返回self的情况下,如何返回实例的内容呢?而self就是pickle正在尝试序列化的那个实例,而它又不能在不调用__getstate__的情况下完成这个操作。注意到__setstate__实际上是将一个B的实例作为状态的一部分传入的。

嗯,这个方法有效……但有人能解释一下为什么吗?这是一个特性还是一个bug?

2 个回答

3

我来给你解释一下这个内容。使用 __slots__ 的类是为了确保没有意外的属性出现。跟普通的 Python 对象不同,使用了 slots 的对象不能动态添加属性。

当 Python 反序列化一个带有 __slots__ 的对象时,它不会假设序列化版本中的属性和你当前的类是兼容的。所以这个责任就交给你了,你可以实现 __getstate____setstate__ 方法。

但是你实现的 __getstate____setstate__ 的方式,似乎绕过了这个检查。下面的代码就是引发这个异常的地方:

try:
    getstate = self.__getstate__
except AttributeError:
    if getattr(self, "__slots__", None):
        raise TypeError("a class that defines __slots__ without "
                        "defining __getstate__ cannot be pickled")
    try:
        dict = self.__dict__
    except AttributeError:
        dict = None
else:
    dict = getstate()

换句话说,你是在告诉 Pickle 模块忽略它的限制,正常地序列化和反序列化你的对象。

这样做可能好也可能不好——我不太确定。但如果你改变了类的定义,然后反序列化一个属性集和你当前类不匹配的对象,这可能会给你带来麻烦。

这就是为什么在使用 slots 时,__getstate____setstate__ 应该更明确。我建议你明确表示你只是把字典的键值对来回传递,比如这样:

class B(A):
    __slots__ = ["porridge"]
    def __getstate__(self):
        return dict(self), self.porridge 
    def __setstate__(self, state):
        self.update(state[0])
        self.porridge = state[1]

注意 dict(self)——这会把你的对象转换成一个字典,确保你状态元组中的第一个元素仅仅是你的字典数据。

20

也许我来得有点晚,但这个问题没有得到一个真正解释发生了什么的答案,所以我来补充一下。

对于那些不想读完整个帖子的人,这里有个简短的总结(帖子有点长……):

  1. __getstate__() 中,你不需要处理包含的 dict 实例——pickle 会为你处理这个。

  2. 如果你还是把 self 包含在状态中,pickle 的循环检测会防止无限循环。

为从 dict 派生的自定义类编写 __getstate__()__setstate__() 方法

让我们先来看看如何正确编写你类的 __getstate__()__setstate__() 方法。你不需要处理 B 实例中包含的 dict 实例的序列化——pickle 知道如何处理字典,会为你搞定。所以这个实现就足够了:

class B(A):
    __slots__ = ["porridge"]
    def __getstate__(self):
        return self.porridge 
    def __setstate__(self, state):
        self.porridge = state

示例:

>>> a = B("oats")
>>> a[42] = "answer"
>>> b = pickle.loads(pickle.dumps(a))
>>> b
{42: 'answer'}
>>> b.porridge
'oats'

你的实现到底发生了什么?

为什么你的实现也能工作,背后又发生了什么?这有点复杂,但——一旦我们知道字典反正会被序列化——就不难理解了。如果 pickle 模块遇到一个用户定义的类的实例,它会调用这个类的 __reduce__() 方法,而这个方法又会调用 __getstate__()(实际上,它通常会调用 __reduce_ex__() 方法,但这里不重要)。让我们重新定义 B,就像你最初那样,即使用 __getstate__() 的“递归”定义,看看现在调用 __reduce__() 会得到什么:

>>> a = B("oats")
>>> a[42] = "answer"
>>> a.__reduce__()
(<function _reconstructor at 0xb7478454>,
 (<class '__main__.B'>, <type 'dict'>, {42: 'answer'}),
 ({42: 'answer'}, 'oats'))

这个文档中可以看到,__reduce__() 方法返回一个包含 2 到 5 个元素的元组。第一个元素是一个函数,用于在反序列化时重建实例,第二个元素是将传递给这个函数的参数元组,第三个元素是 __getstate__() 的返回值。我们可以看到字典信息被包含了两次。_reconstructor() 函数是 copy_reg 模块的一个内部函数,它在反序列化时会在调用 __setstate__() 之前重建基类。(如果你愿意,可以看看这个函数的源代码——很短!)

现在,序列化器需要序列化 a.__reduce__() 的返回值。它基本上是一个接一个地序列化这个元组的三个元素。第二个元素又是一个元组,它的项也一个一个地被序列化。这个内层元组的第三个项(即 a.__reduce__()[1][2])是 dict 类型,并使用字典的内部序列化器进行序列化。外层元组的第三个元素(即 a.__reduce__()[2])也是一个元组,由 B 实例本身和一个字符串组成。在序列化 B 实例时,pickle 模块的循环检测 启动了:pickle 意识到这个实例已经被处理过,只存储它的 id() 的引用,而不是实际序列化它——这就是为什么不会出现无限循环。

当再次反序列化这些内容时,反序列化器首先从流中读取重建函数及其参数。这个函数被调用,结果是一个字典部分已经初始化的 B 实例。接下来,反序列化器读取状态。它遇到一个元组,包含一个已经反序列化的对象的引用——也就是我们的 B 实例——和一个字符串 "oats"。这个元组现在被传递给 B.__setstate__()。状态的第一个元素和 self 现在是同一个对象,可以通过在你的 __setstate__() 实现中添加这一行来验证

print self is state[0]

(它会打印出 True!)。这一行

self.update(state[0])

因此简单地用自身更新了实例。

撰写回答