为什么pickle的__getstate__方法返回值是要求__getstate__处理的实例本身?
我本来想问“如何对一个继承自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 个回答
我来给你解释一下这个内容。使用 __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)
——这会把你的对象转换成一个字典,确保你状态元组中的第一个元素仅仅是你的字典数据。
也许我来得有点晚,但这个问题没有得到一个真正解释发生了什么的答案,所以我来补充一下。
对于那些不想读完整个帖子的人,这里有个简短的总结(帖子有点长……):
在
__getstate__()
中,你不需要处理包含的dict
实例——pickle
会为你处理这个。如果你还是把
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])
因此简单地用自身更新了实例。