将类与python中的字符串相同地哈希

时间:2018-07-27 15:59:59

标签: python python-3.x dictionary hash

我有一个帮助器类来帮助处理字符串方法。它有很多方法和变量,但我希望基础哈希基于其“主要”字符串的内容。因此,该类看起来类似于以下内容:

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如何散列字符串。无论如何,有没有按照我想要的方式进行这项工作?感谢您提供任何信息。

2 个答案:

答案 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")的信息。


您可能想看看@dataclassnamedtuple或像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指出此方法存在一些错误。请参阅下面的评论(或他的详尽回答),以了解为什么您不应该这样做。它可以工作,但是从表面上讲是危险的,应该避免。