我有一个带有一个参数的函数,该函数应使用int
或None
作为参数。有多种方法可以为这种复合类型创建类型别名:
# test.py
import typing
IntOrNone_1 = typing.TypeVar('IntOrNone_1', int, None)
IntOrNone_2 = typing.Union[int, None]
def my_func1(xyz: IntOrNone_1):
return xyz
def my_func2(xyz: IntOrNone_2):
return xyz
my_func1(12)
my_func1(None)
my_func1(13.7)
my_func1('str')
my_func2(12)
my_func2(None)
my_func2(13.7)
my_func2('str')
这两种方法都可以完成我期望的操作,但是,对应的mypy
错误有点不同,但是基本上具有相同的含义。
test.py:14:错误:“ my_func1”的类型变量“ IntOrNone_1”的值 不能是“浮动”
test.py:15:错误:“ my_func1”的类型变量“ IntOrNone_1”的值 不能是“ str”
test.py:19:错误:“ my_func2”的参数1具有不兼容的类型 “浮动”;预期为“可选[int]”
test.py:20:错误:“ my_func2”的参数1具有不兼容的类型 “ str”;预期为“可选[int]”
我倾向于使用第二种方法,因为它还会报告哪个参数导致了错误。
我想这两种方法真的等效吗?还是其中一种是首选?
答案 0 :(得分:3)
这两种方法远非等效。您应该避免将TypeVars看作只是别名-相反,当您要使函数泛型时,它们是您使用的更特殊的形式。
通过示例最容易解释什么是“泛型函数”。假设您要编写一个函数,该函数接受某个对象(任何对象!)并返回另一个完全相同类型的对象。你会怎么做?
我们可以做的一种方法是尝试使用object
:
def identity(x: object) -> object:
return x
这使我们接近,因为我们的identity
函数至少可以接受任何字面的内容(因为所有类型都从Python中的object
继承)。但是,此解决方案有缺陷:如果我们传入一个int,则会返回object
,这不是我们想要的。
相反,我们需要的是类型检查器了解这两种类型之间存在“关系”的方法。这正是TypeVar
派上用场的地方:
T = TypeVar('T')
def identity(x: T) -> T:
return x
在这种情况下,我们的TypeVar'T'充当“占位符”,可以绑定到我们想要的任何类型。因此,如果我们进行identity(3)
,则T
将被绑定到int
上,因此类型检查器将理解返回类型也必须为{{1 }}!
如果我们在参数类型提示中多次使用int
,则类型检查器将确保每次类型都相同。
那么,下面的表达式在做什么?
T
嗯,事实证明,有时在我们的特殊占位符类型中添加 constraints 很有用。例如,您已约束IntOrNone_1 = typing.TypeVar('IntOrNone_1', int, None)
,使其只能绑定到IntOrNone_1
或int
,而不能绑定其他类型。
最后,要回答最后一个问题:在给出的示例中,您绝对应该使用Union,而不是TypeVars。
对于Union,使用Union还是类型别名确实是个人喜好的问题,但是,如果您不需要这种“占位符”或“通用”行为,则不应使用TypeVars。
如果您想了解有关如何使用TypeVars的更多信息,则mypy文档具有section on generics。它涵盖了我跳过的几件事,包括如何制作通用类(不仅仅是通用函数)。