23

The python documentation mentions that if you override __eq__ and the object is immutable, you should also override __hash__ in order for the class to be properly hashable.

In practice, when I do this I often end up with code like

class MyClass(object):
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def __eq__(self, other):
        if type(other) is type(self):
            return (self.a == other.a) and (self.b == other.b)
        else:
            return False

    def __hash__(self):
        return hash((self.a, self.b))

This is somewhat repetitive, and there is a clear risk of forgetting to update one when the other is updated.

Is there a recommended way of implementing these methods together?

Jonas Adler
  • 10,365
  • 5
  • 46
  • 73

3 Answers3

33

Answering my own question. It seems one way of performing this is to define an auxillary __members function and to use that in defining __hash__ and __eq__. This way, there is no duplication:

class MyClass(object):
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def __members(self):
        return (self.a, self.b)

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

    def __hash__(self):
        return hash(self.__members())
Jonas Adler
  • 10,365
  • 5
  • 46
  • 73
  • 4
    there's a risk here that `self.a` or `self.b` change, which causes the hash to change (which breaks all sorts of things) – raylu Dec 10 '19 at 01:37
  • @raylu can you explain more ? – jossefaz Apr 06 '22 at 09:34
  • `one = MyClass(1, 1); stuff = {one}; print(one in stuff); print(MyClass(2, 2) in stuff); one.a = one.b = 2; print(MyClass(2, 2) in stuff); print(MyClass(2, 2) in list(stuff))` this prints `True`, `False`, `False`, `True`. (2, 2) is "not" in the set `stuff` but it's in `list(stuff)`, which doesn't use `__hash__` – raylu Apr 07 '22 at 19:55
  • As mentioned [here](https://stackoverflow.com/a/26641360/4368898), `vars(self)` or `self.__dict__` could be used instead of manually defining `self.__members`. But note that they both return dictionaries which are mutable and not hashable. To use a dictionary with `__hash__`, review the end of [this answer](https://stackoverflow.com/a/25176504/4368898): `hash(tuple(sorted(self.__dict__.items())))`. – Matt Popovich May 05 '22 at 23:14
-1

Is that the equivalent of this one-liner eq?

   def __eq__(self, other):
       return type(other) is type(self) and (self.a == other.a) and (self.b == other.b)
Rebuttle
  • 1
  • 1
-3

I think you can use user defined hash function is better.

class MyClass(object):

def __init__(self, a, b):
    self.a = a
    self.b = b

def __eq__(self, other):
    if type(other) is type(self):
        return (self.a, self.b) == (other.a, other.b)
    else:
        return NotImplemented

def __hash__(self):
    return hash((self.a, self.b))
tiantanshu
  • 13
  • 3
  • 1
    This will give the wrong result for e.g. `MyClass(2, -2) == MyClass(-2, 2)` because of [hash collision](https://stackoverflow.com/q/21743781/12299000) in CPython. You should never use `hash(a) == hash(b)` to decide whether `a` and `b` are equal. – kaya3 Sep 30 '21 at 02:40
  • Yes, it's my fault. I fixed the code – tiantanshu Oct 09 '21 at 02:47