如何使用结构模式匹配检测可散列类型?

How detect hashable types with structural pattern matching?

使用结构模式匹配,如何编写匹配可散列类型实例的案例?

我试过:

for obj in [], (), set(), frozenset(), 10, None, dict():
    match obj:
        case object(__hash__=_):
            print('Hashable type:  ', type(obj))
        case _:
            print('Unhashable type: ', type(obj))

但是,这得到了错误的答案,因为每种类型都定义了 __hash__ 它是否可散列:

Hashable type:   <class 'list'>
Hashable type:   <class 'tuple'>
Hashable type:   <class 'set'>
Hashable type:   <class 'frozenset'>
Hashable type:   <class 'int'>
Hashable type:   <class 'NoneType'>
Hashable type:   <class 'dict'>

解决方案

Hashable abstract base class in collections.abc 可以识别使用 isinstance(obj, Hashable)issubclass(cls, Hashable) 等测试实现散列的类型。

根据 PEP 622, for a class pattern,“匹配是否成功取决于 isinstance 调用的等价物。”

因此,您可以在 class 模式中直接使用 Hashable

from collections.abc import Hashable

for obj in [], (), set(), frozenset(), 10, None, dict():
    match obj:
        case Hashable():
            print('Hashable type:  ', type(obj))
        case _:
            print('Unhashable type:', type(obj))

这会产生所需的答案:

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'>

对 hash() 的调用可能仍然会失败或无用

Hashable只处理最外层对象的类型。它在“对象的类型实现散列”的意义上报告可散列性,这就是我们通常所说的“元组是可散列的”的意思。这也是抽象基础 classes 和静态类型所使用的相同含义。

虽然 Hashable 检测类型是否实现 _hash_,但它无法知道哈希实际上做了什么,是否会成功,或者是否会给出一致的结果。

例如,散列法给出 float('NaN') 的不一致结果。元组和冻结集通常是可散列的,但如果它们的组件值不可散列,则将无法散列。 class 可以定义 __hash__ 以始终引发异常。

Raymond Hettinger 的答案在有限的情况下有效,但它在 list(类型对象本身)这样的输入上失败,即使 list.__hash__ is None 也是可散列的,以及 ([1, 2], [3, 4]) 这样的输入,即使 tuple.__hash__ is not None.

也是不可散列的

检测对象是否可哈希的最可靠方法始终是尝试对其进行哈希处理。如果你想在 match 语句中这样做,最简单的方法是编写一个守卫:

def hashable(x):
    try:
        hash(x)
    except TypeError:
        return False
    else:
        return True

match x:
    case _ if hashable(x):
        ...
    ...

这只是直接调用 hash(x) 并查看它是否有效,而不是尝试执行结构检查。

如果需要hash又想避免重复计算,可以保存hash(x)结果:

def try_hash(x):
    try:
        return hash(x)
    except TypeError:
        return None

match x:
    case _ if (x_hash := try_hash(x)) is not None:
        ...
    ...