Using structural pattern matching, how do you write a case that matches hashable objects?
CodePudding user response:
Core problem
All classes start out by being hashable because they inherit from object which defines a __hash__()
method based on the object id:
>>> hash(object())
269091997
To become unhashable, classes have to override object and set __hash__
to None
:
class UnhashableTuple(tuple):
__hash__ = None
Structural pattern matching can match cases when the __hash__
attribute is None
, but it doesn't provide a direct way to detect when the attribute is not None
.
Solution
Build two cases. The first case filters-out unhashable instances. This leaves only hashable instances for the second case.
Example
Here we test a number of objects for hashability:
for obj in [], (), set(), frozenset(), 10, None, dict():
match obj:
case object(__hash__=None):
print('Unhashable type:', type(obj))
case object(__hash__=_):
print('Hashable type: ', type(obj))
This outputs:
Unhashable type: <class 'list'>
Hashable type: <class 'tuple'>
Unhashable type: <class 'set'>
Hashable type: <class 'frozenset'>
Hashable type: <class 'int'>
Hashable type: <class 'NoneType'>
Unhashable type: <class 'dict'>
Containers
For containers, the above only detects whether the container is hashable. It is possible that the elements in the container are unhashable.
Fortunately, pattern matching can destructure an object and let you test you test the components as well as the container (using the same technique). This is somewhat messy though.
CodePudding user response:
Raymond Hettinger's answer works in limited cases, but it fails on inputs like list
, which is hashable even though list.__hash__ is None
, and inputs like ([1, 2], [3, 4])
, which is unhashable even though tuple.__hash__ is not None
.
The most reliable way to detect whether an object is hashable is always going to be to try to hash it. If you want to do that in a case
statement, the easiest way would be to write a guard:
def hashable(x):
try:
hash(x)
except TypeError:
return False
else:
return True
match x:
case _ if hashable(x):
...
...
This just directly calls hash(x)
and sees whether it works, instead of trying to perform a structural check.
If you need the hash and want to avoid double-computation, you can save the hash(x)
result:
def try_hash(x):
try:
return hash(x)
except TypeError:
return None
match x:
case _ if (x_hash := try_hash(x)) is not None:
...
...