TypeVar('T', A, B) 和 TypeVar('T', bound=Union[A, B]) 的区别

Joe*_*ley 16 python generics type-hinting

我正在努力弄清楚以下两个TypeVars之间的区别

from typing import TypeVar, Union

class A: pass
class B: pass

T = TypeVar("T", A, B)
T = TypeVar("T", bound=Union[A, B])
Run Code Online (Sandbox Code Playgroud)

有人想启发我吗?

一个我不明白的例子......

T = TypeVar("T", bound=Union[A, B])

class AA(A):
    pass


class X(Generic[T]):
    pass


class XA(X[A]):
    pass


class XAA(X[AA]):
    pass
Run Code Online (Sandbox Code Playgroud)

通过类型检查,但T = TypeVar("T", A, B)失败了

错误:“X”的类型变量“T”的值不能是“AA”

相关:this question on the difference between Union[A, B]andTypeVar("T", A, B)

Mic*_*x2a 28

当您这样做时T = TypeVar("T", bound=Union[A, B]),您是说 T 可以绑定到 的任一Union[A, B]或任何子类型Union[A, B]。它是联合的上限

因此,例如,如果您有一个 type 函数def f(x: T) -> T,则传入以下任何类型的值都是合法的:

  1. Union[A, B](或 A 和 B 的任何子类型的联合,例如Union[A, BChild]
  2. A (或 A 的任何亚型)
  3. B (或 B 的任何亚型)

这就是泛型在大多数编程语言中的行为方式:它们让你强加一个单一的上限。


但是当你这样做时T = TypeVar("T", A, B),你基本上是说T must要么由 A 为上限,要么由 B 为上限。也就是说,不是建立单个上限,而是建立多个上限!

因此,这意味着虽然这将是法律在任何类型的值传递AB进入f,这将不会是合法的通过Union[A, B],因为工会是由A和B.没有上界


例如,假设您有一个可以包含 int 或 strs 的可迭代对象。

如果您希望这个可迭代对象包含任意整数或字符串的混合,您只需要一个Union[int, str]. 例如:

from typing import TypeVar, Union, List, Iterable

mix1: List[Union[int, str]] = [1, "a", 3]
mix2: List[Union[int, str]] = [4, "x", "y"]
all_ints = [1, 2, 3]
all_strs = ["a", "b", "c"]


T1 = TypeVar('T1', bound=Union[int, str])

def concat1(x: Iterable[T1], y: Iterable[T1]) -> List[T1]:
    out: List[T1] = []
    out.extend(x)
    out.extend(y)
    return out

# Type checks
a1 = concat1(mix1, mix2)

# Also type checks (though your type checker may need a hint to deduce
# you really do want a union)
a2: List[Union[int, str]] = concat1(all_ints, all_strs)

# Also type checks
a3 = concat1(all_strs, all_strs)
Run Code Online (Sandbox Code Playgroud)

相反,如果您想强制该函数接受所有整数所有字符串的列表,但绝不接受两者的混合,则需要多个上限。

T2 = TypeVar('T2', int, str)

def concat2(x: Iterable[T2], y: Iterable[T2]) -> List[T2]:
    out: List[T2] = []
    out.extend(x)
    out.extend(y)
    return out

# Does NOT type check
b1 = concat2(mix1, mix2)

# Also does NOT type check
b2 = concat2(all_ints, all_strs)

# But this type checks
b3 = concat2(all_ints, all_ints)
Run Code Online (Sandbox Code Playgroud)


Int*_*rer 6

经过大量阅读后,我相信 mypy 正确地提出了type-varOP问题中的错误:

generics.py:31: 错误:“X”的类型变量“T”的值不能是“AA”

请参阅下面的解释。


第二种情况:TypeVar("T", bound=Union[A, B])

我认为@Michael0x2a 的回答很好地描述了正在发生的事情。


第一个案例:TypeVar("T", A, B)

原因归结为里氏替换原则(LSP),也称为行为子类型化。解释这一点超出了本答案的范围,您需要阅读并理解invariancevs的含义covariance

来自python 的typing文档TypeVar

默认情况下,类型变量是不变的。

基于此信息,T = TypeVar("T", A, B)意味着类型变量具有类和T的值限制,但因为它是不变的......它只接受这两个(而不是或 的任何子类)。ABAB

因此,当传递时AA, mypy 会正确地引发type-var错误。


然后您可能会说:好吧,这与AA的行为子类型不正确匹配吗A?在我看来,你是对的。

为什么?因为我们可以正确地替换 out 和Awith AA,并且程序的行为将保持不变。

但是,由于 mypy 是静态类型检查器,因此 mypy 无法弄清楚这一点(它无法检查运行时行为)。人们必须通过语法明确地声明协方差covariant=True

另请注意:指定协变时TypeVar,应在类型变量名称中使用后缀_co。这记录在PEP 484中。

from typing import TypeVar, Generic

class A: pass
class AA(A): pass

T_co = TypeVar("T_co", AA, A, covariant=True)

class X(Generic[T_co]): pass

class XA(X[A]): pass
class XAA(X[AA]): pass
Run Code Online (Sandbox Code Playgroud)

输出:Success: no issues found in 1 source file


那你该怎么办?

我会使用TypeVar("T", bound=Union[A, B]), 因为:

  • A并且B不相关
  • 你希望他们的子类被允许

进一步阅读 mypy 中 LSP 相关问题: