DeepSpace
DeepSpace

Reputation: 81664

Inheritance - __hash__ sets to None in a subclass

I managed to reproduce this on both Python 3.4 and 3.7.

Consider:

class Comparable:
    def _key(self):
        raise NotImplementedError

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

    def __eq__(self, other):
        ...

    def __lt__(self, other):
        ...


class A(Comparable): pass

class B(A):
    def __str__(self):
        return "d"

    def __eq__(self, other):
        return isinstance(self, type(other))

    def _key(self):
        return str(self),

b = B()

Clearly one would expect b.__hash__ to be defined here, since it is defined under Comparable which B is a subclass of.

Lo and behold, it is defined, but evaluates to None. What gives?

>> b
<__main__.B object at 0x00000183C9734978>
>> '__hash__' in dir(b)
True
>> b.__hash__

>> b.__hash__ is None
True
>> B.__mro__
(<class '__main__.B'>, <class '__main__.A'>, <class '__main__.Comparable'>, <class 'object'>)
>> isinstance(b, Comparable)
True

The same behavior is reproduced if implementing __init__ as super().__init__() in Comparable and A.

Upvotes: 29

Views: 5395

Answers (3)

Susanne Oberhauser
Susanne Oberhauser

Reputation: 511

you can nowadays inject __hash__ (and __eq__, ...) into derived classes by modifying the class of the derived class type, once the derived class has been created, like so:

class Base:
    def __hash__(self) -> int: return ...
    def __post_init__(cls):
        cls.__hash__ = Base.__hash__

class Derived(Base):
    ...

d = Derived()
hash(d) # this will call Base.__hash__(d)

This was added in python 3.6

Upvotes: 3

retsigam
retsigam

Reputation: 669

A little late to the game here but I had the same problem. In my case, I also have 197 classes that derive from an abstract base class. I didn't want to copy-paste a ton of instances of...

def __hash__(self) -> int:
    return hash('HASH LOGIC GOES HERE')

...so I did the following, which works for me:

class Base:
    def __init__(self):
        self.__class__.__hash__ = Base.__hash__  # <----- SOLUTION


    def __hash__(self) -> int:
        return hash('HASH LOGIC GOES HERE')


class Derived(Base):
    def __eq__(self, other) -> bool:
        return isinstance(other, Derived)  # or whatever logic


if __name__ == '__main__':
    derived = Derived()
    print(f'derived.__hash__: {derived.__hash__}')
    print(f'hash(derived):    {hash(derived)}')

Upvotes: 3

kabanus
kabanus

Reputation: 25980

Found it in the docs:

A class that overrides __eq__() and does not define __hash__() will have its __hash__() implicitly set to None.

and

If a class that overrides __eq__() needs to retain the implementation of __hash__() from a parent class, the interpreter must be told this explicitly by setting __hash__ = <ParentClass>.__hash__

From ticket 1549:

This was done intentionally -- if you define a comparison without defining a hash, the default hash will not match your comparison, and your objects will misbehave when used as dictionary keys.

(Guido van Rossum)

Upvotes: 40

Related Questions