Mypy - 为什么 TypeVar 在没有指定界限的情况下不起作用

问题描述

我正在尝试理解类型注释,我有以下代码

from typing import TypeVar

T = TypeVar('T')

class MyClass():
    x: int = 10

def foo(obj: T) -> None:
    print(obj.x)

foo(MyClass())

运行 mypy 时出现以下错误

main.py:9: error: "T" has no attribute "x"
Found 1 error in 1 file (checked 1 source file)

但是当我添加

bound='MyClass'

到 TypeVar,它显示没有错误

这种行为的原因是什么?我试图阅读文档,但没有找到有关将 bound 设置为认值时究竟发生了什么的任何答案。

解决方法

这不是 TypeVar 通常的用途。

以下函数是 TypeVar 通常用于的函数类型的一个很好的例子:

def baz(obj):
    return obj

此函数可处理任何类型的参数,因此注释此函数的一种解决方案可能是使用 typing.Any,如下所示:

from typing import Any

def baz(obj: Any) -> Any:
    return obj

然而,这并不好。我们通常应该仅将 Any 用作最后的手段,因为它不会向类型检查器提供有关我们代码中变量的任何信息。如果我们过于随意地使用 Any,许多潜在的错误将被忽视,因为类型检查器实际上会放弃,而不检查我们代码的那部分。

在这种情况下,我们可以向类型检查器提供更多信息。我们不知道输入参数的类型是什么,也不知道返回类型是什么,但我们知道输入类型和返回类型是相同的,不管它们是什么是。我们可以通过使用 TypeVar:

来显示类型之间的这种关系——类型依赖关系
from typing import TypeVar

T = TypeVar('T')

def baz(obj: T) -> T:
    return obj

我们也可以在类似但更复杂的情况下使用 TypeVar。考虑这个函数,它将接受任何类型的序列,并使用该序列构造一个字典:

def bar(some_sequence):
    return {some_sequence.index(elem): elem for elem in some_sequence}

我们可以这样注释这个函数:

from typing import TypeVar,Sequence

V = TypeVar('V')

def bar(some_sequence: Sequence[V]) -> dict[int,V]:
    return {some_sequence.index(elem): elem for elem in some_sequence}

无论some_sequence元素的推断类型是什么,我们都可以保证返回的字典的值是相同的类型。

绑定TypeVar

绑定 TypeVar 当我们有一个像上面那样具有某种类型依赖的函数时很有用,但我们想进一步缩小所涉及的类型。例如,想象以下代码:

class BreakfastFood:
    pass


class Spam(BreakfastFood):
    pass


class Bacon(BreakfastFood):
    pass


def breakfast_selection(food):
    if not isinstance(food,BreakfastFood):
        raise TypeError("NO.")
    # do some more stuff here
    return food

在这段代码中,我们有一个类型依赖,就像前面的例子一样,但有一个额外的复杂性:如果传递给它的参数不是一个实例,该函数将抛出一个 TypeError —或子类的实例 — BreakfastFood 类。为了让这个函数通过类型检查器,我们需要将我们使用的 TypeVar 限制为 BreakfastFood 及其子类。我们可以通过使用 bound 关键字参数来做到这一点:

from typing import TypeVar 


class BreakfastFood:
    pass


B = TypeVar('B',bound=BreakfastFood)


class Spam(BreakfastFood):
    pass


class Bacon(BreakfastFood):
    pass


def breakfast_selection(food: B) -> B:
    if not isinstance(food,BreakfastFood):
        raise TypeError("NO.")
    # do some more stuff here
    return food

您的代码发生了什么

如果您使用未绑定的 obj 注释 foo 函数中的 TypeVar 参数,您是在告诉类型检查器 obj 可以是任何类型。但是类型检查器在这里正确地引发了一个错误:您已经告诉它 obj 可以是任何类型,但您的函数假定 obj 具有属性 x,而不是全部python 中的对象具有 x 属性。通过将 T TypeVar 绑定到 — — 以及 — MyClass 的子类的实例,我们告诉类型检查器 obj 参数应该是 {{1} 的实例},或MyClass的子类的实例。 MyClass 及其子类的所有实例都具有 MyClass 属性,因此类型检查器很高兴。万岁!

但是,在我看来,您当前的函数根本不应该真正使用 x,因为您的函数注释中不涉及类型依赖性。如果您知道 TypeVar 参数应该是 — obj 的实例或子类的实例,并且您的注释中没有类型依赖性,那么您可以简单地直接注释您的函数MyClass

MyClass

另一方面,如果 class MyClass: x: int = 10 def foo(obj: MyClass) -> None: print(obj.x) foo(MyClass()) 不需要是 MyClass 的实例或子类的实例,并且实际上任何具有 obj 属性的类都可以,那么您可以使用 x 来指定:

typing.Protocol

完全解释 from typing import Protocol class SupportsXAttr(Protocol): x: int class MyClass: x: int = 10 def foo(obj: SupportsXAttr) -> None: print(obj.x) foo(MyClass()) 超出了这个已经很长的答案的范围,但 here's 是一篇很棒的博文。