关于Python字典的键的一些研究

  • 前言
  • 搬运
  • 疑惑
  • 研究
  • 其他思考


前言

今天10月28日,RNG2:0二刷4AM战队,杀象杀象!!
起因是摸鱼的时候看了这篇文章,一些有趣且鲜为人知的 Python 特性,觉得很神奇就深入研究了下关于Python字典的 key

搬运

可能有人跟我一样懒得点链接,那我就搬过来好了。

class SomeClass(str):
    pass

some_dict = {'s':42}

Output:

>>> type(list(some_dict.keys())[0])
str
>>> s = SomeClass('s')
>>> some_dict[s] = 40
>>> some_dict # 预期: 两个不同的键值对
{'s': 40}
>>> type(list(some_dict.keys())[0])
str

说明:

  • 由于 SomeClass 会从 str 自动继承 __hash__ 方法, 所以 s 对象和 “s” 字符串的哈希值是相同的.
  • 而 SomeClass(“s”) == “s” 为 True 是因为 SomeClass 也继承了 str 类 __eq__ 方法.
  • 由于两者的哈希值相同且相等, 所以它们在字典中表示相同的键.
  • 如果想要实现期望的功能, 我们可以重定义 SomeClass 的 __eq__ 方法.
class SomeClass(str):
  def __eq__(self, other):
      return (
          type(self) is SomeClass
          and type(other) is SomeClass
          and super().__eq__(other)
      )

  # 当我们自定义 __eq__ 方法时, Python 不会再自动继承 __hash__ 方法
  # 所以我们也需要定义它
  __hash__ = str.__hash__

some_dict = {'s':42}

Output:

>>> s = SomeClass('s')
>>> some_dict[s] = 40
>>> some_dict
{'s': 40, 's': 42}
>>> keys = list(some_dict.keys())
>>> type(keys[0]), type(keys[1])
(__main__.SomeClass, str)

(完)

疑惑

众所周知,字典的键值必须是 hashable 的也就是可哈希的对象,那么如果想要我们自己定义的对象作字典的值,就必须继承或者自定义特殊方法 __hash__。
在上面的例子里面,SomeClass 继承于 str,因此 SomeClass('s') 的哈希值肯定与 's' 的哈希值相等。
我以为这样就够了,以为键与键之间的比较通过比较哈希值是否相等就够了,结果上面说明里的 2/3 点还提到了值的比较,难道我一直以来的认知是错误的?于是我开始了搜索和实验。

研究

下面我自己声明了一个类:

class SomeClass():
    def __init__(self, value):
        self.value = value
    def __repr__(self):
        return repr(self.value)
    def __hash__(self):
        return hash(self.value)

开始了实验:

s = SomeClass('s')
some_dict = {'s':42}
some_dict[s] = 3
print(hash('s')==hash(s))
# True
print('s'==s)
# False
print(some_dict)
# {'s': 42, 's': 3}

果然,当只有哈希值相等而值不相等时,字典会认作两个键。
下面我再类里面添加了魔术方法 __eq__ :

def __eq__(self, other):
        return self.value == other if type(self.value)==type(other) else False

然后这次的输出:

s = SomeClass('s')
some_dict = {'s':42}
some_dict[s] = 3
print(hash('s')==hash(s))
# True
print('s'==s)
# True
print(some_dict)
# {'s': 3}

果然,只有两者的哈希值相同且相等,它们在字典中才表示相同的键!

其他思考

  1. 为啥非要我在类里面补充 __eq__方法之后才会比较?
    因为 str 类的 __eq__ 方法里面只实现了相同类(也就是两个字符串)之间的比较,对于其他类的比较会返回 NotImplemented ,于是会调用 SomeClass 的 __eq__ 方法进行比较。