如果 __hash__ 被覆盖,Pickle/dill 无法处理循环引用

Pickle/dill cannot handle circular references if __hash__ is overridden

考虑以下 MWE:

#import dill as pickle      # Dill exhibits similar behavior
import pickle

class B:
    def __init__(self):
        self.links = set()

class A:
    def __init__(self, base: B):
        self.base = base
        base.links.add(self)

    def __hash__(self):
        return hash(self.base)

    def __eq__(self, other):
        return self.base == other.base

pickled = pickle.dumps(A(B()))  # Success
print(pickle.loads(pickled))    # Not so much

以上示例失败,出现以下异常:

Traceback (most recent call last):
  File "./mwe.py", line 26, in <module>
    print(pickle.loads(pickled))
  File "./mwe.py", line 18, in __hash__
    return hash(self.base)
AttributeError: 'A' object has no attribute 'base'

据我了解,pickle 会在反序列化 A 之前尝试反序列化 B.linksB 中使用的 set 实例试图在某个时候调用 A.__hash__,并且由于 A 的实例尚未完全构建,它无法计算自己的哈希值,使得大家都很难过

如何在不破坏循环引用的情况下解决这个问题? (打破循环将需要大量工作,因为我要序列化的对象非常复杂)

我认为您已经正确地确定了问题的原因。这两个实例都相互依赖,pickle 无法以正确的顺序初始化它们。这可能被认为是一个错误,但幸运的是有一个简单的解决方法。

Pickle 允许我们使用 __getstate__ and __setstate__ 函数自定义对象的腌制方式。我们可以使用它在散列之前手动设置 A 实例缺少的 base 属性:

class B:
    def __init__(self):
        self.links = set()

    def __getstate__(self):
        # dump a tuple instead of a set so that the __hash__ function won't be called
        return tuple(self.links)

    def __setstate__(self, state):
        self.links= set()
        for link in state:
            link.base= self # set the missing attribute
            self.links.add(link) # now it can be hashed