谈谈__eq__和__hash__

今天刷一道算法题的时候用到了list_a == list_b的判断,==is大家都已经是耳熟能详了,前者是判断值是否相等,后者是判断引用是否相等,在用==操作符进行判断的时候,其实内部调用的是__eq__方法。比如

class Item:
  def init(self, val):
      self.val = val
  def eq(self, other):
      return self.val == other.val

first = Item('hello') second = Item('hello) print(first == second) # True

如果不实现__eq__方法,那么自定义类型会调用默认的__eq__方法, 通过默认方法进行比较的相等条件相当严格,只有自己和自己比才会返回True,表现如下

class Item:
  def init(self, val):
      self.val = val

first = Item('hello') second = Item('hello) print(first == second) # False

因此,在需要进行自定义类型比较的时候,建议实现__eq__方法。

谈及__eq__方法,就不得不谈__hash__,两者总是一起出现.在Python中,如果自定义类定义了__eq__而未定义__hash__方法的话,那么默认将__hash__方法设置为None。这会有什么潜在问题呢?

Python中的对象分为可变和不可变对象,我们从另一个角度来看,可以分为可哈希对象和不可哈希对象。通俗的说,可哈希对象可以作为字典的键,不可哈希对象无法作为字典的键。有时候,我们使用列表或者自定义对象作为字典的键,或者使用set进行元素去重的时候,会遇到unhashable type: xxx之类的问题,这类问题出现的原因就是字典的键或者集合中的元素类型为不可哈希类型。

那么常见的不可哈希类型有哪些呢?几乎都是一些常见的可变类型,比如列表、集合和字典等,都是不可哈希类型。有时候我们有对元素类型为不可变类型的对象进行去重或者使用它作为字典的key的需求,这个时候又该怎么做呢?

我们可以自定义一个类,来实现__eq____hash__方法达到这个效果,且看下面代码

class It(list):
    def __init__(self, vals):
        self.vals = vals
    def __eq__(self, other):
        return self.vals == other.vals
    def __hash__(self):
        # 注意__hash__需要返回一个整数
        return hash(';'.join(vals))

s = set()
j = It(['a', 'b'])
s.add(j)
print(j in set) # True
k = It(['a', 'b'])
print(k in set) # True

从上面结果我们可以看到k这个实例并未加入s这个集合,但是在判断时返回了True,原因就是jk两者的__hash____eq__运算结果相同

我们改改代码,再看看结果

class It(list):
    def __init__(self, vals):
        self.vals = vals
    def __eq__(self, other):
        # 这里如果用 self == other就会出现无限递归,读者可以思考为什么
        return id(self) == id(other)
    def __hash__(self):
        # 注意__hash__需要返回一个整数
        return hash(';'.join(vals))

s = set()
j = It(['a', 'b'])
s.add(j)
print(j in set) # True
k = It(['a', 'b'])
print(k in set) # False

可以看到在关于通过hash运算判断两个对象是否映射成一个值是需要__hash____eq__方法共同决定的