Reputation: 81664
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
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
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
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