我有一个帮助器类来帮助处理字符串方法。它有很多方法和变量,但我希望基础哈希基于其“主要”字符串的内容。因此,该类看起来类似于以下内容:
class Topic:
def __init__(self, name):
self.name = name
def getName(self):
return self.name
def setName(self, newName):
self.name = newName
def __str__(self):
return self.name
但是我希望字典将这个对象作为字符串散列,所以当我执行以下代码时:
a = Topic('test')
v = {a : 'oh hey'}
print(v[Topic('test')])
我希望它打印“哦嘿”,而不是抛出键错误。我尝试对我的Topic类执行此操作:
def __hash__(self):
return hash(self.name)
但是它没有用,我在网上找不到Python如何散列字符串。无论如何,有没有按照我想要的方式进行这项工作?感谢您提供任何信息。
答案 0 :(得分:2)
如果您阅读the documentation on __hash__
,它将说明发生了什么以及如何解决:
如果一个类没有定义
__eq__()
方法,它也不应该定义__hash__()
操作…
如果两个值哈希相同但不相等,就字典而言,它们不是相同的键,则它们是碰巧发生哈希冲突的两个不同值。因此,您的Topic
值仍然由身份作为关键字(您只能使用完全相同的实例查找Topic
,而不能使用名称相同的另一个实例),这只是在降低效率。
要解决此问题,您想添加一个__eq__
方法,如果两个Topic
具有相同的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
或者,如果您想使两个具有相同名称的Topic
像同一个键一样工作,而不是名称本身,则需要将__hash__
更改为以下形式:
def __hash__(self):
return hash((type(self), self.name))
因此,两个名称为Topic
的{{1}}值都将被散列为'spam'
,并且将彼此匹配,但不会匹配(Topic, "spam")
的哈希本身。
第二个问题更严重。
您的"spam"
对象是可变的。实际上,通过使用getter和setter(在Python中通常是不需要的),您明确地要求人们希望能够使Topic
的{{1}}发生变异。 / p>
但是,如果这样做,则相同的name
将不再具有相同的哈希值,并且不再等于其原始值。这会破坏您要放入的任何词典。
Topic
在同一文档中对此进行了介绍:
如果类定义了可变对象并实现了
Topic
方法,则它不应该实现>>> v = {a: 'oh hey'} >>> a.setName('test2') >>> v KeyError: <__main__.Topic object at 0x12370b0b8>
,因为可哈希集合的实现要求键的哈希值是不可变的(如果对象的哈希值更改了) ,它将位于错误的哈希存储桶中。
这就是为什么唯一可哈希化的内置集合是不可变集合的原因。
有时,这值得颠覆。如果您的类型通常是可变的,但是您知道在存储或查询字典后永远不会改变其中的一种,那么基本上可以欺骗Python并告诉您您的类型是不可变的,因此可以通过定义__eq__()
和__hash__()
作为dict键来用作dict键,如果您对该对象进行了突变,它们将会中断,但不会中断,因为您永远不会这样做那个。
但是通常,您希望遵循以下规则:如果您希望某物成为键,则它应该是不变的。
通常,仅使它“按照约定不可变”就足够了。例如,如果您通过将__hash__
重命名为__eq__
来使name
成为“按惯例私有”,并且放弃了_name
方法,而现有的类只有setName
,并添加了getName
和__hash__
方法)。当然,有人可以通过从下面改变私有属性的值来破坏您的命令,但是您可以期望您的用户是“同意成年人”,除非他们有充分的理由,否则不要这样做。 >
当我们讨论时,还有最后一件事:您几乎总是想为这样的类定义一个__eq__
。注意我们上面提到的关于__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")
的信息。
您可能想看看@dataclass
,namedtuple
或像attrs
这样的第三方库,它们可以自动编写所有这些方法-<__main__.Topic object at 0x12370b0b8>
, __init__
,__hash__
,__eq__
等为您服务,并确保它们都能正常工作。
例如,这可以替换整个类的定义:
__repr__
由于它是@dataclass(frozen=True)
class Topic:
name: str
,它将使用其属性的元组(即frozen
)进行哈希和比较。
答案 1 :(得分:1)
为了使Python自定义的哈希值能够实现某种功能,我们不仅需要为其提供自定义的哈希函数,还需要使其能够与相同类型的另一个版本进行比较,因此更新后的代码如下:如下:
class Topic:
def __init__(self, name):
self.name = name;
def getName(self):
return self.name
def setName(self, newName):
self.name = newName
def __str__(self):
return self.name;
def __eq__(self, other):
return self.name == other.name
def __hash__(self):
return hash(self.name)
编辑:
@abarnert指出此方法存在一些错误。请参阅下面的评论(或他的详尽回答),以了解为什么您不应该这样做。它可以工作,但是从表面上讲是危险的,应该避免。