如何在 Mypy 中使用 __subclasshook__?

How to use __subclasshook__ with Mypy?

为什么在 Mypy 下,__subclasshook__ 适用于来自 collections.abc 的一招小马,但不适用于用户定义的 类?

比如这个程序

from collections.abc import Hashable

class A:
    def __hash__(self) -> int:
        return 0

a: Hashable = A()

产出

$ mypy demo.py --strict
Success: no issues found in 1 source file

但是这个等效程序

from abc import ABCMeta, abstractmethod

def _check_methods(C: type, *methods: str) -> bool:
    mro = C.__mro__
    for method in methods:
        for B in mro:
            if method in B.__dict__:
                if B.__dict__[method] is None:
                    return NotImplemented
                break
        else:
            return NotImplemented
    return True

class Hashable(metaclass=ABCMeta):
    __slots__ = ()

    @abstractmethod
    def __hash__(self) -> int:
        return 0

    @classmethod
    def __subclasshook__(cls, C: type) -> bool:
        if cls is Hashable:
            return _check_methods(C, "__hash__")
        return NotImplemented

class A:
    def __hash__(self) -> int:
        return 0

a: Hashable = A()

产出

$ mypy demo.py --strict
demo.py:32: error: Incompatible types in assignment (expression has type "A", variable has type "Hashable")
Found 1 error in 1 file (checked 1 source file)

Mypy 是否以特殊方式处理一招小马?

是的,mypy 将这些 class 视为特殊情况。请记住,mypy 用于 static 类型检查,这意味着它无需 运行 您的代码即可工作,仅分析源代码。它实际上从未 调用 __subclasshook__ 或类似的东西来确定什么是可散列的或不可散列的。您的“等效” class 仅在运行时等效,因为它依赖于 __subclasshook__ 被调用。

如果你想 mypy 处理它不知道的东西,你必须写一个 mypy plugin 来处理它。

Mypy 不使用标准库的实现,而是使用 typeshed 包中的规范(“存根文件”)。在这个包中,collections.abc.Hashable 是一个 typing.Protocol.

typeshed/stdlib/_collections_abc.pyi:

from typing import (
    AbstractSet as Set,
    AsyncGenerator as AsyncGenerator,
    AsyncIterable as AsyncIterable,
    AsyncIterator as AsyncIterator,
    Awaitable as Awaitable,
    ByteString as ByteString,
    Callable as Callable,
    Collection as Collection,
    Container as Container,
    Coroutine as Coroutine,
    Generator as Generator,
    Generic,
    Hashable as Hashable,
    ItemsView as ItemsView,
    Iterable as Iterable,
    Iterator as Iterator,
    KeysView as KeysView,
    Mapping as Mapping,
    MappingView as MappingView,
    MutableMapping as MutableMapping,
    MutableSequence as MutableSequence,
    MutableSet as MutableSet,
    Reversible as Reversible,
    Sequence as Sequence,
    Sized as Sized,
    TypeVar,
    ValuesView as ValuesView,
)

typeshed/stdlib/typing.pyi:

@runtime_checkable
class Hashable(Protocol, metaclass=ABCMeta):
    # TODO: This is special, in that a subclass of a hashable class may not be hashable
    #   (for example, list vs. object). It's not obvious how to represent this. This class
    #   is currently mostly useless for static checking.
    @abstractmethod
    def __hash__(self) -> int: ...