TypeVar('T', A, B) 和 TypeVar('T', bound=Union[A, B]) 之间的区别是什么?

65
以下两个TypeVar之间有什么区别?
from typing import TypeVar, Union

class A: pass
class B: pass

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

我相信在Python 3.12中,这就是这两个边界之间的区别。
class Foo[T: (A, B)]: ...
class Foo[T: A | B]: ...

这是一个我不明白的例子:这通过了类型检查...
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

...但是使用T = TypeVar("T", A, B)时,它会失败,并显示以下错误信息:

错误:类型变量"T"的值在"X"中不能为"AA"


相关:关于Union[A, B]TypeVar("T", A, B)之间的区别,可以参考这个问题

@Carcigenicate -- 关于你的第一条评论,类型检查器始终进行子类型检查,无论你使用什么类型变量或者是否使用泛型。实际上,几乎所有具有命名子类型的类型系统都会这样做--例如,Java和C++。你的示例不起作用的原因是,虽然MyUnion可能是Union[int, str]的子类型,但它不是int的子类型。 - Michael0x2a
关于您的第三条评论,Union[A, B]是符合PEP 484标准的有效绑定,因为该类型不包含任何类型变量--类型变量是通过使用TypeVar创建的类型。例如,如果您执行了T1 = TypeVar('T1'),则在另一个TypeVar定义中尝试使用T1将是非法的,例如通过执行T2 = TypeVar('T2', bound=T2)T3 = TypeVar('T3', T2, int)。这种限制主要存在是为了使类型检查器不需要实现高阶类型,这是一种相当复杂的类型系统特性。 - Michael0x2a
还有第三个选项:T = TypeVar("T", Union[A, B]) - pabouk - Ukraine stay strong
1
@mypy 不允许单个约束。 - joel
2个回答

97
当你执行T = TypeVar("T", bound=Union[A, B])时,你在说T可以绑定到Union[A, B]或任何Union[A, B]的子类型。它是对联合类型的上界进行限制。
因此,例如,如果你有一个类型为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必须是由A或B上界。也就是说,与其建立一个单一的上界,你可以建立多个上界!
因此,这意味着虽然可以将类型为A或B的值传递给f,但无法将Union[A, B]传递给它,因为该联合既不是A的上界也不是B的上界。
例如,假设你有一个可包含整数或字符串的可迭代对象。
如果你想让这个可迭代对象包含任意混合的整数或字符串,你只需要一个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)

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

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)

@JoelB -- 我正在使用Python 3.7,但切换到Python 3.6似乎没有什么区别--例如,请参见https://mypy-play.net/?mypy=0.761&python=3.6&gist=73d96dee6e7ffc814d7deb7ec59e32bd。 - Michael0x2a
1
是的,我看到了3.6和3.7的错误。这个错误仅出现在TypeVar("T", A, B)中。 - joel
如果我有一个抽象类 Base,我能否创建一个 TypeVar,它绑定到任何一个子类,但不是它们的联合,而无需显式地对它们进行类型标注(这样当添加更多子类时,我就不需要更新 TypeVar)? - Jackson H
多么棒的答案! - Yonatan
我在这里提交了一个相关的问题评论:https://github.com/microsoft/pyright/issues/744#issuecomment-1627773919 - nh2
显示剩余4条评论

6

经过大量阅读,我相信mypy在OP的问题中正确地引发了type-var错误:

generics.py:31: error: "X"的类型变量"T"的值不能为"AA"

请参见以下解释。


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

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


第一种情况:TypeVar("T", A, B)

原因在于里氏替换原则(LSP),也称为行为子类型化。解释这个原则超出了本答案的范围,您需要阅读并理解不变性协变性的含义。

根据Python的typing文档中对TypeVar的描述

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

基于这些信息,T = TypeVar("T", A, B)表示类型变量T具有类AB的值限制,但由于它是不变的...它只接受这两个类(而不是AB的任何子类)。

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


你可能会问:那么,AA 是否能够正确匹配 A 的行为子类型呢?在我看来,你是正确的。为什么呢?因为可以用 AA 替换 A,程序的行为不会改变。但是,由于 mypy 是一个静态类型检查器,它无法找出这一点(无法检查运行时行为)。必须通过语法 covariant=True 显式地声明协变性。还要注意:当指定协变的 TypeVar 时,应该在类型变量名称中使用后缀 _co。这在 PEP 484 here 中有说明。
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

输出:

输出:成功:在1个源文件中未发现问题


那么,你应该怎么做呢?

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

  • AB没有关联
  • 你希望允许它们的子类

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


当我在函数中使用T = TypeVar("T", A, B)并放置一个A的子类,而该函数期望类型为T时,mypy --strict不会显示错误。此外,在Python文档(https://docs.python.org/3/library/typing.html#typing.TypeVar)中指出:“还要注意,如果参数是某个str子类的实例,则返回类型仍然是普通的str。”这似乎表明子类型对于他们的示例是可以的,这与我所说的情况相同。有人知道原因吗? - xuiqzy
我不确定 @xuiqzy,你能否通过 GitHub Gist 等方式分享一个最小化的可复现代码呢? - Intrastellar Explorer
查看mypy源代码可以发现允许子类型。 - ktb
T_co = TypeVar("T_co", AA, A) 通过 mypy 检查同样顺畅。在 Python 3.9 和 3.10 上测试通过。 - bravmi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接