为什么pickle __getstate__接受一个返回值,它首先需要__getstate__来进行pickle的实例?

时间:2011-03-09 14:31:47

标签: python pickle

我打算问“如何挑选一个继承自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的情况下返回实例的内容,这是pickle已经尝试腌制的实例,并且不能做所以没有打电话给__getstate__。请注意__setstate__实际上是如何接收实例B作为州的一部分。

嗯,它有效......但是有人可以解释为什么吗?它是一个功能还是一个bug?

2 个答案:

答案 0 :(得分:14)

也许我参加派对有点晚了,但是这个问题没有得到实际解释发生了什么的答案,所以我们走了。

以下是那些不想阅读整篇文章的人的快速摘要(它有点长......):

  1. 您无需关注dict中包含的__getstate__()个实例 - pickle会为您执行此操作。

  2. 如果你在状态中包含selfpickle的周期检测将阻止无限循环。

  3. __getstate__()

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

    让我们从正确的方式开始编写班级的__getstate__()__setstate__()方法。您不需要处理dict实例中包含的B实例的内容 - 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__()(实际上,它通常调用{{1}方法,但这并不重要)。让我们像你原来那样再次定义__reduce_ex__(),即使用B的“recurisve”定义,让我们看看我们现在为__getstate__()的{​​{1}}实例调用__reduce__()时得到了什么:

    B

    the documentation of __reduce__()我们可以看到,该方法返回一个2到5个元素的元组。第一个元素是一个函数,在unpickling时将被调用以重构实例,第二个元素是将传递给此函数的参数元组,第三个元素是>>> a = B("oats") >>> a[42] = "answer" >>> a.__reduce__() (<function _reconstructor at 0xb7478454>, (<class '__main__.B'>, <type 'dict'>, {42: 'answer'}), ({42: 'answer'}, 'oats')) 的返回值。我们已经可以看到字典信息被包含两次。函数__getstate__()_reconstructor()模块的内部函数,它在unpickling调用copy_reg之前重构基类。 (如果您愿意,请查看source code of this function - 它很简短!)

    现在,pickler需要挑选__setstate__()的返回值。它基本上一个接一个地腌制这个元组的三个元素。第二个元素是一个元组,它的项目也一个接一个地腌制。此内部元组的第三项(即a.__reduce__())的类型为a.__reduce__()[1][2],并使用内部pickler进行字典腌制。外元组的第三个元素(即dict)也是一个元组,由a.__reduce__()[2]实例本身和一个字符串组成。在挑选B个实例时,cycle detection of the pickle module开始执行:B意识到这个确切的实例已经被处理过,并且只存储对其pickle的引用而不是真正的酸洗它 - 这就是没有infinte循环的原因。

    当再次解开这个混乱时,unpickler首先从流中读取重建函数及其参数。调用该函数,导致id()实例已经初始化了字典部分。接下来,unpickler读取状态。它遇到一个元组,该元组由对已经未打开的对象的引用组成 - 即我们的B实例 - 和一个字符串B。此元组现在传递给"oats"B.__setstate__()state的第一个元素现在是同一个对象,可以通过添加行看到

    self

    print self is state[0] 实施(打印__setstate__()!)。这条线

    True

    因此只需用自身更新实例。

答案 1 :(得分:3)

这是我理解的想法。如果您的班级使用__slots__,那么这是一种保证没有任何意外属性的方法。与常规Python对象不同,使用插槽实现的对象不能动态添加属性。

当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模块将其反对意见放在一边,并正常序列化和反序列化你的对象。

这可能是也可能不是一个好主意 - 我不确定。但是我想如果你改变你的类定义然后用一组不同于你的运行时类所期望的属性来反序列化一个对象,那么我可能会回过头来咬你。

这就是为什么,特别是在使用广告位时,您的__getstate____getstate__应该更明确。我会明确地说明你只是来回发送字典键/值,如下所示:

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) - 将你的对象转换为dict,这应该确保你的状态元组中的第一个元素只是你的字典数据。