【问题标题】:Hashing a class identically to a string in python将一个类与python中的字符串相同地散列
【发布时间】:2019-01-04 19:03:24
【问题描述】:

我有一个帮助类来帮助处理字符串方法。它有一堆方法和变量,但我希望底层哈希基于其“主”字符串的内容。所以这个类看起来类似于这样:

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')])

我希望它打印“哦,嘿”而不是抛出关键错误。我尝试对我的主题类这样做:

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

但它不起作用,我在网上找不到 Python 如何散列他们的字符串。反正有没有按照我的意图进行这项工作?感谢您提供任何信息。

【问题讨论】:

  • 我希望hash(self.name) 可以正常工作,有什么错误?请注意,您还需要实现 __eq__ 才能将某些内容放入字典中。
  • 不,已经有很多例子了。参见例如stackoverflow.com/q/7560172/3001761, stackoverflow.com/q/4901815/3001761.
  • 这可能是个坏主意。如果你使用一个主题作为键,然后在它上面调用setName,你就破坏了字典。它以旧名称被抨击,但不再具有该哈希值。这就是为什么在内置容器中,只有不可变的容器是可散列的。你有一个显式的 name 设置器(在 Python 中几乎从不需要或不需要 getter 和 setter)这一事实表明你希望人们修改这些东西并破坏你的字典。

标签: python python-3.x dictionary hash


【解决方案1】:

如果您阅读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 中通常不需要),您明确要求人们能够改变 nameTopic

但如果你这样做,相同的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__。注意到我们上面抱怨&lt;__main__.Topic object at 0x12370b0b8&gt; 的错误了吗?同样,如果您只是在交互式提示下评估aprint(v),即使没有任何问题,Topic 也会像这样显示。那是因为__str__ 只影响str,而不影响repr。通常的模式是:

def __repr__(self):
    return f"{type(self).__name__}({self.name!r})"

现在,您将看到类似 Topic("spam") 的内容,而不是 &lt;__main__.Topic object at 0x12370b0b8&gt;


你可能想看看@dataclassnamedtuple,或者像attrs这样可以自动编写所有这些方法的第三方库——__init____hash____eq____repr__ 和其他人 - 为您服务,并确保他们都能正常工作。

例如,这可以替换您的整个类定义:

@dataclass(frozen=True)
class Topic:
    name: str

因为它是frozen,它将使用其属性的元组(即name)进行散列和比较。

【讨论】:

  • 我希望我不会太笨,这会帮助一些人。我搜索了一会儿,即使我知道那里有答案,但在我所问的内容附近找不到任何东西。这是非常感谢的,让我对这种情况有了更多的了解,甚至比我希望得到的还要多。谢谢!
  • @Luke 是的,信息隐藏在参考文档中,没有多少人为了好玩而阅读。 :) 但是确实值得阅读有关您要覆盖的特殊方法的文档,因为那里有很多不明显的东西。
【解决方案2】:

为了使 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 指出了这种方法的一些非常错误的地方。请参阅下面的 cmets(或他非常彻底的答案)以了解您为什么不应该这样做。它会起作用,但它具有欺骗性的危险性,应该避免。

【讨论】:

  • 此代码仅在您从未致电 setName 时才有效。尝试将其中一些作为键插入到您的字典中,然后在它们上调用setName,然后尝试查找它们,迭代字典,添加一个具有旧键名称的新键,等等。
  • 另外:您的主题将与匹配其name 的字符串相同,但它不会等于该字符串,只会与另一个具有相同名称的主题相同。那真的是你想要的吗?通常对于这样的情况,您希望两个具有相同名称的主题彼此散列相同,但您不希望它们与名称本身散列相同。你可以通过散列其他东西来做到这一点,比如type(self),再加上self.name(只需散列两个东西的元组来组合散列)。
  • @abarnert 如果您想将所有这些信息添加到答案中以便我可以接受,我很乐意这样做。作为来自 Java 的人,这让我大开眼界。我可以将您的 cmets 添加到我现有的答案中,但我更愿意归功于您。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-03-17
  • 2020-03-12
  • 2015-04-09
  • 1970-01-01
  • 2022-01-25
  • 2015-12-14
  • 2011-01-04
相关资源
最近更新 更多