如何正确键入 Python 方法返回带有通用类型实例及其通用类型 属性 的字典?

How to properly type Python methods returning a dict with a generically typed instance and its generically typed property?

我有以下 Python 函数并希望以 mypy 理解的方式输入它 return 类型作为字典,项目的类型作为值类型,id 类型作为键类型:

def id_map(items):
    return {item.id: item for item in items}

当前的方法如下所示:

from typing import Iterable, Protocol, TypeVar

S = TypeVar("S", covariant=True, bound="ClassWithId")
T = TypeVar("T", covariant=True)

class ClassWithId(Protocol[S, T]):
    def __new__(cls: type[S], *args: Any, **kwargs: Any) -> "ClassWithId[S, T]":
        ...

    @property
    def id(self: S) -> T:
        ...


def id_map(items: Iterable[ClassWithId[S, T]]) -> dict[T, S]:
    return {item.id: cast(S, item) for item in items}

这当前导致类型 builtins.dict[Any, Any]。我想不出一个不同的实现方式来实现它。 我想知道使用 mypy 是否完全可行。

TLDR:删除自身类型变量 S。它不是必需的,但会导致类型检查器不太可能理解的类型关系。

class ClassWithId(Protocol[T]):
    @property
    def id(self) -> T:
        ...

类型变量可以以两种方式使用:作为调用中的推断变量或作为泛型类型中的声明变量。前者用于受使用上下文约束的类型,后者用于必须在整个 class.

中保持一致的类型

S 类型的 var 是 Self 类型;这总是可以从用法中推断出来,不需要在类型本身中重复。否则,您构造的递归类型可能会混淆当前的类型检查器。

从 generic/protocol 中删除 S 表明它实际上并未用于任何类型关系: T 完全受 class [=19= 约束].无需参考S来定义输入和输出之间的任何关系。

class ClassWithId(Protocol[T]):
    @property
    def id(self: S) -> T: ...
    #                  ^ defined by generic class, not S

注意 id_map 也不依赖于 S——知道 TClassWithId[T] 中的任何一个就足以了解另一个。因此,可以完全删除 S

T = TypeVar("T", covariant=True)

class ClassWithId(Protocol[T]):
    @property
    def id(self) -> T:
        ...

def id_map(items: Iterable[ClassWithId[T]]) -> dict[T, ClassWithId[T]]:
    return {item.id: item for item in items}

如果 id_map 应该保留 确切的 ClassWithId[T] 而不仅仅是协议,这对应于 更高级的类型。原则上,这由具有协议范围的类型变量表示。

CWI = TypeVar("CWI", bound=ClassWithId)

def id_map(items: Iterable[CWI[T]]) -> dict[T, CWI[T]]:
    return {item.id: item for item in items}

然而,MyPy does not supported higher kinded types at the moment.