如果您阅读the documentation on __hash__,它会解释发生了什么以及如何解决它:
如果一个类没有定义__eq__() 方法,它也不应该定义__hash__() 操作……
如果两个值的哈希值相同但不相等,那么就 dict 而言,它们不是同一个键,它们是碰巧发生哈希冲突的两个不同值。因此,您的 Topic 值仍以身份为键(您只能查找具有完全相同实例的 Topic,而不是具有相同名称的另一个实例),您只是降低了它的效率。
要解决此问题,您需要添加一个__eq__ 方法,如果两个Topics 具有相同的name,则它们相等。
def __eq__(self, other):
return self.name == other.name
但是这样做有两个问题。
首先,您的 Topic 对象现在将与它们的名称相同,但它们将不等于它们。这可能不是你想要的。
如果您希望仅使用字符串作为键来查找主题,则需要更改 __eq__ 方法来处理:
def __eq__(self, other):
return self.name == other or self.name == other.name
或者,如果您希望两个具有相同名称的 Topics 像同一个键一样工作,但不是名称本身,您需要将 __hash__ 更改为如下内容:
def __hash__(self):
return hash((type(self), self.name))
因此,名称为 'spam' 的两个 Topic 值都将被哈希为 (Topic, "spam"),并且会相互匹配,但不会匹配 "spam" 本身的哈希。
第二个问题更严重。
您的 Topic 对象是可变的。实际上,通过使用 getter 和 setter(在 Python 中通常不需要),您明确要求人们能够改变 name 的 Topic。
但如果你这样做,相同的Topic 不再具有相同的哈希值,并且不再等于其原始值。这会破坏您放入的任何字典。
>>> v = {a: 'oh hey'}
>>> a.setName('test2')
>>> v
KeyError: <__main__.Topic object at 0x12370b0b8>
这在相同的文档中有所介绍:
如果一个类定义了可变对象并实现了__eq__()方法,它不应该实现__hash__(),因为hashable集合的实现要求key的hash值是不可变的(如果对象的hash值改变了在错误的哈希桶中)。
这就是为什么唯一可散列的内置集合是不可变的。
有时候,这是值得颠覆的。如果你有一个通常是可变的类型,但你知道在它存储或在字典中查找后你永远不会改变其中一个,基本上你可以对 Python 撒谎并告诉它你的类型是不可变的,因此通过定义 __hash__ 和 __eq__ 来适合作为 dict 键,如果您对对象进行变异,会中断,但不会因为您永远不会这样做而中断。 p>
但通常情况下,您希望遵循这样的规则:如果您希望某物成为键,则它应该是不可变的。
通常只需使其“按照惯例不可变”就足够了。例如,如果您通过将 name 重命名为 _name 来使 name“按约定私有”,并摆脱 setName 方法并只有 getName,那么您现有的类(添加了 __hash__ 和 @ 987654358@ 方法)很好。当然,有人可以通过从你下面更改私有属性的值来破坏你的命令,但你可以期望你的用户是“同意的成年人”,除非他们有充分的理由,否则不要这样做。 p>
最后一件事,当我们在做的时候:你几乎总是想为这样的类定义一个__repr__。注意到我们上面抱怨<__main__.Topic object at 0x12370b0b8> 的错误了吗?同样,如果您只是在交互式提示下评估a 或print(v),即使没有任何问题,Topic 也会像这样显示。那是因为__str__ 只影响str,而不影响repr。通常的模式是:
def __repr__(self):
return f"{type(self).__name__}({self.name!r})"
现在,您将看到类似 Topic("spam") 的内容,而不是 <__main__.Topic object at 0x12370b0b8>。
你可能想看看@dataclass,namedtuple,或者像attrs这样可以自动编写所有这些方法的第三方库——__init__,__hash__,__eq__, __repr__ 和其他人 - 为您服务,并确保他们都能正常工作。
例如,这可以替换您的整个类定义:
@dataclass(frozen=True)
class Topic:
name: str
因为它是frozen,它将使用其属性的元组(即name)进行散列和比较。