为什么在该可哈希对象的集合中找不到我的可哈希对象,该集合是另一个对象的属性?

Why is my hashable object not found in a set of that hashable object, the set being an attribute of another object?

我在两个 类 的对象之间有一个递归关系:Foo 有一个 setBar 对象(在它的 bars 属性中)和每个 Bar 都有 listFoo 对象(在其 foos 属性中)。我已经实现如下 MWE 所示,但测试失败。为什么?

根据Python Glossary,一个set只能包含hashable个对象和hashable个对象:

An object is hashable if it has a hash value which never changes during its lifetime (it needs a __hash__() method), and can be compared to other objects (it needs an __eq__() method).

我真的不知道我下面的 MWE 是否满足对象具有永不更改的散列,因为散列取决于列表中的其他对象和设置属性。有没有办法解决这个问题?

最小工作示例:

import unittest


class Test(unittest.TestCase):
    def test_foo_bar(self):
        foo = Foo()
        bar = Bar()
        bar.add_foo(foo)

        print(bar)
        print(foo.bars)
        print(hash(bar))
        for bar in foo.bars:
            print(hash(bar))
        # The previous print lines print the following:
        # <mwe2.Bar object at 0x105ba8080>
        # {<mwe2.Bar object at 0x105ba8080>}
        # -9223096319794529578
        # -9223096319794529578

        # The following assertion is OK
        self.assertTrue(bar in {bar})

        # The following assertion fails with AssertionError: False is not true
        self.assertTrue(bar in foo.bars)



class Foo:
    def __init__(self):
        self.bars = set()

    def __hash__(self) -> int:
        return hash(self.__dict__.values())


class Bar:
    def __init__(self):
        self.foos = list()

    def __hash__(self) -> int:
        return hash(tuple(self.foos))

    def add_foo(self, foo: "Foo"):
        foo.bars.add(self)
        self.foos.append(foo)


if __name__ == '__main__':
    unittest.main()

我用CPython,Python3.6.x.

问题是您的 Bar 的哈希值在您将其添加到 Foo.bars 后立即发生变化。如果在 add_foo 方法中添加一些 print 语句,您可以看到这一点:

def add_foo(self, foo: "Foo"):
    foo.bars.add(self)
    print(hash(self))
    self.foos.append(foo)
    print(hash(self))

输出:

3527539
957074234

这是因为散列是根据self.foos计算的,所以对self.foos的任何修改也会改变对象的散列。

(旁注:与问题中所述相反,bar in {bar} 的计算结果如您所料 True 。没有理由不这样做。我怀疑有调试时出现某种错误。)


使单元测试工作的一个简单方法是交换 add_foo 中的两行代码:

def add_foo(self, foo: "Foo"):
    self.foos.append(foo)  # append first
    foo.bars.add(self)  # add to the set with the new hash

输出:

Ran 1 test in 0.000s

OK

但是,这不是真正的解决方法:如果您多次调用 add_foo,它也无济于事。如果您在将 Bar 对象添加到集合或字典后调用 add_foo,您将再次遇到同样的问题。

我认为很明显,相互依赖 类 且散列不一致是一个糟糕的设计选择。可能的解决方案包括

  1. 删除 Foo -> Bar -> Foo 依赖循环
  2. 寻找一致的散列方法