Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 563
DeepSpace Avatar asked Nov 28 '18 11:11

DeepSpace


People also ask

Is subclass multiple inheritance?

Object-Oriented Programming Multiple inheritance means that a subclass can inherit from two or more superclasses. C++ allows multiple inheritance, but Java allows only single inheritance, that is, a subclass can inherit only one superclass.

Does a subclass inherit attributes?

A subclass “inherits” all the attributes (methods, etc) of the parent class. This means that a subclass will have everything that its “parents” have. You can then change (“override”) some or all of the attributes to change the behavior.


2 Answers

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)

like image 167
kabanus Avatar answered Oct 10 '22 07:10

kabanus


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)}')
like image 26
retsigam Avatar answered Oct 10 '22 09:10

retsigam