Home > Mobile >  Using structural pattern matching to detect hashability
Using structural pattern matching to detect hashability

Time:11-05

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:
        ...
    ...
  • Related