类型检查与运行时
写完这篇之后,我终于理解了@Alexander 在第一条评论中的观点:无论你在注释中写什么,它都不会影响运行时,并且你的代码以相同的方式执行(抱歉,我错过了你不是在看类型检查看法)。这是 python 类型的核心原则,而不是强类型语言(这使它在 IMO 中非常棒):你总是可以说“我在这里不需要类型——节省我的时间和精神健康”。类型注释用于帮助一些第三方工具,如mypy(由 python 核心团队维护的类型检查器)和 IDE。 IDE 可以根据这些信息向您提出一些建议,mypy 会检查您的代码是否可以在您的类型与实际相符的情况下工作。
通用版
T = TypeVar('T')
class Stack(Generic[T]):
def __init__(self) -> None:
self.items: list[T] = []
def push(self, item: T) -> None:
self.items.append(item)
def pop(self) -> T:
return self.items.pop()
def empty(self) -> bool:
return not self.items
您可以像对待常规变量一样对待类型变量,但在运行时用于“元”用法并被忽略(好吧,有一些运行时跟踪,但它们主要用于内省目的)。它们为每个绑定上下文替换一次(更多信息 - 下文),并且每个模块范围只能定义一次。
上面的代码声明了带有一个类型参数的普通泛型类。现在你可以说 Stack[int] 来引用一堆整数,这很棒。当前定义允许显式键入或使用隐式 Any 参数化:
# Explicit type
int_stack: Stack[int] = Stack()
reveal_type(int_stack) # N: revealed type is "__main__.Stack[builtins.int]
int_stack.push(1) # ok
int_stack.push('foo') # E: Argument 1 to "push" of "Stack" has incompatible type "str"; expected "int" [arg-type]
reveal_type(int_stack.pop()) # N: revealed type is "builtins.int"
# No type results in mypy error, similar to `x = []`
any_stack = Stack() # E: need type annotation for any_stack
# But if you ignore it, the type becomes `Stack[Any]`
reveal_type(any_stack) # N: revealed type is "__main__.Stack[Any]
any_stack.push(1) # ok
any_stack.push('foo') # ok too
reveal_type(any_stack.pop()) # N: revealed type is "Any"
为了使预期的使用更容易,您可以允许从 iterable 进行初始化(我没有涵盖您应该使用 collections.deque 而不是 list 和可能而不是这个 Stack 类的事实,假设它只是一个玩具集合):
from collections.abc import Iterable
class Stack(Generic[T]):
def __init__(self, items: Iterable[T] | None) -> None:
# Create an empty list with items of type T
self.items: list[T] = list(items or [])
...
deduced_int_stack = Stack([1])
reveal_type(deduced_int_stack) # N: revealed type is "__main__.Stack[builtins.int]"
总而言之,泛型类有一些绑定到类主体的类型变量。当您创建此类的实例时,它可以用某种类型进行参数化 - 它可能是另一种类型变量或某种固定类型,如 int 或 tuple[str, Callable[[], MyClass[bool]]]。然后在其主体中出现的所有 T(嵌套类除外,它们可能不在“快速浏览”解释上下文中)都替换为该类型(或 Any,如果未给出且无法推导)。当且仅当__init__ 或__new__ 参数中的至少一个具有引用T 的类型(只是T 或说list[T]),否则您必须指定它。请注意,如果您在非泛型类的 __init__ 中使用了 T,这不是很酷,尽管目前不被禁止。
现在,如果您在泛型类的某些方法中使用 T,如果传递的类型与预期不兼容,它会引用该替换值并导致类型检查错误。
你可以玩这个例子here。
在通用上下文之外工作
但是,并非类型变量的所有用法都与泛型类相关。幸运的是,您不能在调用端声明通用 arg 的可能性来声明通用函数(如 function<T> fun(x: number): int 和 fun<string>(0)),但还有更多的东西。让我们从更简单的例子开始——纯函数:
T = TypeVar('T')
def func1() -> T:
return 1
def func2(x: T) -> int:
return 1
def func3(x: T) -> T:
return x
def func4(x: T, y: T) -> int:
return 1
第一个函数被声明为返回一些值未绑定输入 T。这显然没有意义,最近的mypy 版本甚至学会了将其标记为错误。您的函数返回仅取决于参数和外部状态 - 类型变量必须存在,对吗?您也不能在模块范围内声明类型为T 的全局变量,因为T 仍未绑定 - 因此func1 args 和模块范围的变量都不能依赖于T。
第二个功能更有趣。它不会导致mypy错误,尽管仍然没有多大意义:我们可以将一些类型绑定到T,但这和func2_1(x: Any) -> int: ...有什么区别?我们可以推测现在 T 可以用作函数体中的注释,这可以在某些类型变量具有上限的极端情况下提供帮助,我不会说这是不可能的 - 但我不能快速构造这样的例子,并且从未在适当的上下文中看到过这种用法(这总是一个错误)。类似的示例甚至在 PEP 中被明确引用为有效。
第三个和第四个函数是函数中类型变量的典型例子。第三个声明函数返回与其参数相同的类型。
第四个函数接受两个相同类型的参数(任意一个)。如果您有 T = TypeVar('T', bound=Something) 或 T = TypeVar('T', str, bytes),它会更有用:您可以连接两个类型为 T 的参数,但不能连接类型为 str | bytes 的两个参数,如下例所示:
T = TypeVar('T', str, bytes)
def total_length(x: T, y: T) -> int:
return len(x + y)
关于本节中上述所有示例的最重要事实:T 对于不同的函数不必相同。您可以拨打func3(1),然后是func3(['bar']),然后是func4('foo', 'bar')。 T 在这些调用中是 int、list[str] 和 str - 无需匹配。
考虑到这一点,您的第二个解决方案很明确:
T = TypeVar('T')
class Stack:
def __init__(self) -> None:
# Create an empty list with items of type T
self.items: list[T] = [] # E: Type variable "__main__.T" is unbound [valid-type]
def push(self, item: T) -> None:
self.items.append(item)
def pop(self) -> T: # E: A function returning TypeVar should receive at least one argument containing the same TypeVar [type-var]
return self.items.pop()
这里是mypy问题,讨论similar case。
__init__ 说我们将属性 x 设置为类型 T 的值,但是这个 T 稍后丢失(T 仅在 __init__ 范围内) - 所以 mypy 拒绝分配。
push 格式不正确,T 在这里没有意义,但不会导致输入无效的情况,因此不会被拒绝(参数类型被擦除为 Any,因此您仍然可以调用 push一些争论)。
pop 无效,因为类型检查器需要知道 my_stack.pop() 将返回什么。它可以说“我放弃了 - 只要有你的任何”,并且将完全有效(PEP 不强制执行此操作)。但 mypy 更聪明,拒绝设计无效的使用。
边缘情况:您可以返回 SomeGeneric[T] 和未绑定的 T,例如,在工厂函数中:
def make_list() -> list[T]: ...
mylist: list[str] = make_list()
因为否则无法在调用站点上指定类型参数
为了更好地理解 python 中的类型变量和泛型,我建议您阅读 PEP483 和 PEP484 - 通常 PEP 更像是一个无聊的标准,但这些确实是一个很好的起点。
那里省略了许多边缘情况,这仍然在 mypy 团队(可能还有其他类型检查器)中引起热烈讨论 - 例如,泛型类的静态方法中的类型变量,或用作构造函数的类方法中的绑定 - 请注意它们可以被使用在实例上也是如此。但是,基本上你可以:
- 将 TypeVar 绑定到类(
Generic 或 Protocol,或某些 Generic 子类 - 如果您对 Iterable[T] 进行子类化,则您的类在 T 中已经是通用的) - 然后所有方法都使用相同的 @ 987654396@ 并且可以包含在一侧或两侧
- 或者有一个方法作用域/函数作用域的类型变量——如果在签名中重复不止一次,它就会很有用。