类型的种类(kinds)¶
到目前为止,我们主要限制在内置类型。此部分介绍几种额外类型。您可能需要其中的一些来进行任何非平凡程序的类型检查。
Class 类型¶
每个类(class)也是有效类型。任何子类(subclass)的实例也与所有超类兼容——因此,所有值都与 object
类型兼容(顺便提一下,也与后面讨论的 Any
类型兼容)。Mypy 分析类的主体,以确定实例中可用的方法和属性。以下示例使用了子类:
class A:
def f(self) -> int: # self 的类型推断为 (A)
return 2
class B(A):
def f(self) -> int:
return 3
def g(self) -> int:
return 4
def foo(a: A) -> None:
print(a.f()) # 3
a.g() # 错误:“A”没有属性“g”
foo(B()) # OK(B 是 A 的子类)
Any 类型¶
带有 Any
类型的值是动态类型的。Mypy 无法了解此类值的可能运行时类型。对该值的任何操作都是允许的,这些操作仅在运行时检查。当您无法使用更精确的类型时,可以将 Any
作为“逃生阀”。
Any
与每个其他类型兼容,反之亦然。您可以自由地将 Any
类型的值赋给更精确类型的变量:
a: Any = None
s: str = ''
a = 2 # OK(将 "int" 赋给 "Any")
s = a # OK(将 "Any" 赋给 "str")
声明(和推断)类型在运行时被忽略(或 擦除)。它们基本上被视为注释,因此上述代码不会生成运行时错误,即使 s
在运行时获取了 int
值,而 s
的声明类型实际上是 str
!使用 Any
类型时需要小心,因为它们允许您对 mypy 进行“欺骗”,这可能会隐藏错误。
如果您未定义函数的返回值或参数类型,则这些默认都是 Any
:
def show_heading(s) -> None:
print('=== ' + s + ' ===') # 没有静态类型检查,因为 s 的类型是 Any
show_heading(1) # OK(仅在运行时出错;mypy 不会生成错误)
您应该给静态类型函数显式的 None
返回类型,即使它不返回值,因为这让 mypy 捕获额外的类型错误:
def wait(t: float): # 隐式 Any 返回值
print('Waiting...')
time.sleep(t)
if wait(2) > 1: # Mypy 没有捕获这个错误!
...
如果我们使用显式的 None
返回类型,mypy 将捕获错误:
def wait(t: float) -> None:
print('Waiting...')
time.sleep(t)
if wait(2) > 1: # 错误:无法比较 None 和 int
...
Any
类型在 动态类型代码(Dynamically) 部分有更详细的讨论。
备注
没有任何类型的函数签名是动态类型的。动态类型函数的主体不会进行静态检查,局部变量具有隐式 Any
类型。这使得将遗留 Python 代码迁移到 mypy 更加容易,因为 mypy 不会对动态类型函数进行抱怨。
Tuple 类型¶
类型 tuple[T1, ..., Tn]
表示具有项类型 T1
、...、Tn
的元组:
# 在 Python 3.8 及更早版本中使用 `typing.Tuple`
def f(t: tuple[int, str]) -> None:
t = 1, 'foo' # OK
t = 'foo', 1 # 类型检查错误
这种元组类型具有确切的特定项数(上例中为 2)。元组也可以用作不可变的可变长度序列。您可以使用类型 tuple[T, ...]
(带有文字 ...
,这是语法的一部分)来实现此目的。例如:
def print_squared(t: tuple[int, ...]) -> None:
for n in t:
print(n, n ** 2)
print_squared(()) # OK
print_squared((1, 3, 5)) # OK
print_squared([1, 2]) # 错误:仅元组有效
备注
通常,使用 Sequence[T]
而不是 tuple[T, ...]
更好,因为
Sequence
也与列表和其他非元组序列兼容。
备注
tuple[...]
在 Python 3.6 及更高版本中作为基类是有效的,并且在存根文件中始终有效。在早期的 Python 版本中,您有时可以通过使用命名元组作为基类来解决此限制(见 Named tuples 部分)。
Callable 类型(以及 lambdas)¶
您可以在静态类型代码中传递函数对象和绑定方法。接受参数 A1
、...、An
并返回 Rt
的函数类型为 Callable[[A1, ..., An], Rt]
。示例:
from collections.abc import Callable
def twice(i: int, next: Callable[[int], int]) -> int:
return next(next(i))
def add(i: int) -> int:
return i + 1
print(twice(3, add)) # 5
备注
如果您使用 Python 3.8 或更早版本,请从 typing
导入
Callable[...]
而不是 collections.abc
。
可调用类型只能包含位置参数,且只能是没有默认值的参数。这涵盖了大多数可调用类型的用法,但有时这并不够。Mypy 识别一种特殊形式 Callable[..., T]
(带有文字 ...
),可以用于不太典型的情况。它与返回类型与 T
兼容的任意可调用对象兼容,无论参数的数量、类型或种类如何。Mypy 允许您使用任意参数调用这样的可调用值,而不进行任何检查——在这方面,它们的处理方式类似于 (*args: Any, **kwargs: Any)
的函数签名。示例:
from collections.abc import Callable
def arbitrary_call(f: Callable[..., int]) -> int:
return f('x') + f(y=2) # OK
arbitrary_call(ord) # 无静态错误,但在运行时失败
arbitrary_call(open) # 错误:不返回 int
arbitrary_call(1) # 错误:'int' 不是可调用的
在需要更精确或复杂的回调类型的情况下,可以使用灵活的 回调协议。匿名函数也受到支持。匿名函数的参数和返回值类型不能显式给出;它们始终根据上下文使用双向类型推断进行推断:
l = map(lambda x: x + 1, [1, 2, 3]) # 推断 x 为 int,l 为 list[int]
如果您想显式给出参数或返回值类型,可以使用普通的、可能是嵌套的函数定义。
可调用类型也可以用于类型对象,匹配它们的 __init__
或 __new__
签名:
from collections.abc import Callable
class C:
def __init__(self, app: str) -> None:
pass
CallableType = Callable[[str], C]
def class_or_callable(arg: CallableType) -> None:
inst = arg("my_app")
reveal_type(inst) # 推断类型为 "C"
这在您希望 arg
既可以是返回 C
实例的 Callable
,又可以是 C
本身的类型时非常有用。这同样适用于 回调协议。
Union 类型¶
Python 函数通常接受两种或多种不同类型的值。您可以使用 重载 来表示这一点,但联合类型通常更方便。
使用 T1 | ... | Tn
构造一个联合类型。例如,如果一个参数的类型为 int | str
,那么整数和字符串都是有效的参数值。
您可以使用 isinstance()
检查来将联合类型缩小到更具体的类型:
def f(x: int | str) -> None:
x + 1 # 错误:str + int 不是有效的
if isinstance(x, int):
# 这里 x 的类型是 int。
x + 1 # 正确
else:
# 这里 x 的类型是 str。
x + 'a' # 正确
f(1) # 正确
f('x') # 正确
f(1.1) # 错误
备注
只有当操作对 每个 联合项都是有效时,联合类型的操作才是有效的。这就是为什么通常需要使用 isinstance()
检查先将联合类型缩小到非联合类型。这也意味着建议避免将联合类型用作函数的返回类型,因为调用者可能需要在对值进行任何有意义的操作之前使用 isinstance()
。
Python 3.9 及更早版本只部分支持此语法。相反,您可以使用传统的 Union[T1, ..., Tn]
类型构造函数。示例:
from typing import Union
def f(x: Union[int, str]) -> None:
...
在不支持运行时新语法的 Python 版本中,如果您使用 from __future__ import annotations
(请参阅 运行时的注解问题(Annotation issues) ),也可以在某些限制下使用新语法:
from __future__ import annotations
def f(x: int | str) -> None: # 在 Python 3.7 及更高版本中有效
...
Optional 和 None 类型¶
您可以使用 T | None
来定义一个允许 None
值的类型变体,例如 int | None
。这被称为 可选类型(optional type):
def strlen(s: str) -> int | None:
if not s:
return None # 正确
return len(s)
def strlen_invalid(s: str) -> int:
if not s:
return None # 错误:None 与 int 不兼容
return len(s)
为了支持 Python 3.9 及更早版本,您可以使用 Optional
类型修饰符,例如 Optional[int]
( Optional[X]
是 Union[X, None]
的首选简写):
from typing import Optional
def strlen(s: str) -> Optional[int]:
...
大多数操作不允许在未保护的 None
或 可选 值(带有可选类型的值)上进行:
def my_inc(x: int | None) -> int:
return x + 1 # 错误:无法将 None 与 int 相加
相反,需要显式的 None
检查。Mypy 拥有强大的类型推断能力,允许您使用常规 Python 习惯来防范 None
值。例如,mypy 识别 is None
检查:
def my_inc(x: int | None) -> int:
if x is None:
return 0
else:
# 在这里,x 的推断类型仅为 int。
return x + 1
由于在 if 条件中检查了 None
,mypy 会推断出 else 块中的 x
类型为 int
。
其他支持的检查以防范 None
值包括 if x is not None
、if x
和 if not x
。此外,mypy 还理解逻辑表达式中的 None
检查:
def concat(x: str | None, y: str | None) -> str | None:
if x is not None and y is not None:
# 此时 x 和 y 都不为 None
return x + y
else:
return None
有时,mypy 不会意识到一个值永远不是 None
。这通常发生在类实例可以处于部分定义状态的情况,其中某个属性在对象构造期间初始化为 None
,但一个方法假设该属性不再是 None
。Mypy 会对此可能的 None
值提出警告。您可以在方法中使用 assert x is not None
来解决此问题:
class Resource:
path: str | None = None
def initialize(self, path: str) -> None:
self.path = path
def read(self) -> str:
# 我们要求对象已初始化。
assert self.path is not None
with open(self.path) as f: # 正确
return f.read()
r = Resource()
r.initialize('/foo/bar')
r.read()
在将变量初始化为 None
时,None
通常是一个空的占位符值,实际值有不同的类型。这就是为什么您需要在像上面的 Resource
类的情况下对属性进行注解:
class Resource:
path: str | None = None
...
这同样适用于在方法中定义的属性:
class Counter:
def __init__(self) -> None:
self.count: int | None = None
通常不使用任何初始值来为属性赋值会更简单。这样,您就不需要使用可选类型,也可以避免 assert ... is not None
检查。如果您在类体中对属性进行了注解,则不需要初始值:
class Container:
items: list[str] # 无初始值
Mypy 通常使用对变量的第一次赋值来推断该变量的类型。然而,如果您在同一作用域内同时赋值 None
值和非 None
值,mypy 通常可以在没有注解的情况下正确处理:
def f(i: int) -> None:
n = None # 推断类型为 'int | None',因为下面的赋值
if i > 0:
n = i
...
有时,您可能会收到错误消息 "无法确定 <something> 的类型"。在这种情况下,您应该添加显式的 ... | None
注解。
备注
None
是只有一个值 None
的类型。None
也用作没有返回值的函数的返回类型,即隐式返回 None
的函数。
备注
Python 解释器在内部使用 NoneType
作为 None
的类型,但在类型注解中始终使用 None
。后者更简短且更易读。( NoneType
在 Python 3.10+ 中作为 types.NoneType
可用,但在早期版本的 Python 中根本没有暴露。)
备注
类型 Optional[T]
并不 意味着带有默认值的函数参数。它仅仅意味着 None
是有效的参数值。这是一个常见的误解,因为 None
是参数的常见默认值,而带有默认值的参数有时被称为 可选(optional) 参数(parameters)(或参数(arguments))。
Type 别名(aliases)¶
在某些情况下,类型名称可能会变得冗长且难以输入,特别是当它们被频繁使用时:
def f() -> list[dict[tuple[int, str], set[int]]] | tuple[str, list[str]]:
...
当出现这种情况时,您可以通过将类型赋值给变量来定义类型别名(这是一种 隐式类型别名):
AliasType = list[dict[tuple[int, str], set[int]]] | tuple[str, list[str]]
# 现在我们可以使用 AliasType 替代完整名称:
def f() -> AliasType:
...
备注
类型别名并不创建新类型。它只是另一种类型的简写符号——它与目标类型等价,除了 generic aliases。
Python 3.12 引入了 type
语句用于定义 显式类型别名。显式类型别名消除了歧义,并通过明确意图来提高可读性:
type AliasType = list[dict[tuple[int, str], set[int]]] | tuple[str, list[str]]
# 现在我们可以使用 AliasType 替代完整名称:
def f() -> AliasType:
...
关于何时定义隐式类型别名可能会产生困惑——例如,当别名包含前向引用、无效类型或违反类型别名声明的其他限制时。由于未注解变量与类型别名之间的区别是隐式的,模棱两可或不正确的类型别名声明默认会定义为普通变量,而不是类型别名。
使用 type
语句定义的别名具有以下特性,这将它们与隐式类型别名区分开来:
定义可以包含前向引用,而无需使用字符串文字转义,因为它是惰性求值的。
别名可以在类型注解、类型参数和类型转换中使用,但不能在需要类对象的上下文中使用。例如,它不能作为基类,也不能用于构造实例。
还有一种旧的语法用于定义显式类型别名,该语法在 Python 3.10 中引入(PEP 613):
from typing import TypeAlias # 在 Python 3.9 及更早版本中使用 "from typing_extensions"
AliasType: TypeAlias = list[dict[tuple[int, str], set[int]]] | tuple[str, list[str]]
Named tuples¶
Mypy 识别命名元组,并可以对定义或使用它们的代码进行类型检查。在这个例子中,我们可以检测尝试访问缺失属性的代码:
Point = namedtuple('Point', ['x', 'y'])
p = Point(x=1, y=2)
print(p.z) # 错误:Point 没有属性 'z'
如果使用 namedtuple
定义命名元组,则所有项被假定为 Any
类型。也就是说,mypy 不知道项的类型。您可以使用 NamedTuple
来定义项类型:
from typing import NamedTuple
Point = NamedTuple('Point', [('x', int),
('y', int)])
p = Point(x=1, y='x') # 参数类型不兼容 "str"; 期望 "int"
Python 3.6 引入了一种替代的基于类的命名元组语法:
from typing import NamedTuple
class Point(NamedTuple):
x: int
y: int
p = Point(x=1, y='x') # 参数类型不兼容 "str"; 期望 "int"
备注
如果任何 NamedTuple
对象有效,您可以在类型注解中使用原始的 NamedTuple
“伪类(pseudo-class)”。
例如,它在反序列化时可能很有用:
def deserialize_named_tuple(arg: NamedTuple) -> Dict[str, Any]:
return arg._asdict()
Point = namedtuple('Point', ['x', 'y'])
Person = NamedTuple('Person', [('name', str), ('age', int)])
deserialize_named_tuple(Point(x=1, y=2)) # 正确
deserialize_named_tuple(Person(name='Nikita', age=18)) # 正确
# 错误:参数 1 的类型 "Tuple[int, int]" 不兼容; 期望 "NamedTuple"
deserialize_named_tuple((1, 2))
请注意,此行为高度实验性,非标准,可能不被其他类型检查器和 IDE 支持。
类对象的类型¶
(自由改编自 PEP 484: 类对象的类型 。)
有时,您想要讨论继承自给定类的类对象。这可以用 type[C]
来表示(在 Python 3.8 及以下版本中使用 typing.Type[C]
),其中 C
是一个类。换句话说,当 C
是类的名称时,使用 C
注解参数表示该参数是 C
的实例(或其子类),而使用 type[C]
作为参数注解表示该参数是派生自 C
的类对象(或 C
本身)。
假设以下类:
class User:
# 定义字段如 name, email
class BasicUser(User):
def upgrade(self):
"""升级到 Pro"""
class ProUser(User):
def pay(self):
"""支付账单"""
注意,ProUser
并不继承自 BasicUser
。
以下是一个函数,如果您传递正确的类对象,它将创建这些类的实例:
def new_user(user_class):
user = user_class()
# (这里我们可以将用户对象写入数据库)
return user
我们该如何注解这个函数呢?如果不能对 type
进行参数化,我们能做的最好的是:
def new_user(user_class: type) -> User:
# 与之前相同的实现
这似乎是合理的,除了在以下示例中,mypy 不知道 buyer
变量的类型是 ProUser
:
buyer = new_user(ProUser)
buyer.pay() # 被拒绝,User 上没有该方法
但是,使用 type[C]
语法和带有上界的类型变量(见 具有上界的类型变量(Type variables)),我们可以做得更好(Python 3.12 语法):
def new_user[U: User](user_class: type[U]) -> U:
# 与之前相同的实现
使用旧版语法(Python 3.11 及以下):
U = TypeVar('U', bound=User)
def new_user(user_class: type[U]) -> U:
# 与之前相同的实现
现在,当我们用 User
的特定子类调用 new_user()
时,mypy 将推断出正确的结果类型:
beginner = new_user(BasicUser) # 推断类型为 BasicUser
beginner.upgrade() # OK
备注
对应于 type[C]
的值必须是实际的类对象,且是 C
的子类型。它的构造函数必须与 C
的构造函数兼容。如果 C
是类型变量,则其上界必须是类对象。
有关 type[]
和 typing.Type[]
的更多详细信息,请参见 PEP 484: 类对象的类型。
生成器(Generators)¶
一个基本的仅用于生成值的生成器,可以简洁地注解为具有返回类型 Iterator[YieldType]
或 Iterable[YieldType]
。例如:
def squares(n: int) -> Iterator[int]:
for i in range(n):
yield i * i
一个好的原则是尽可能地用最具体的返回类型注解函数。然而,您也应该小心避免将实现细节泄露到函数的公共 API 中。遵循这两个原则时,优先选择 Iterator[YieldType]
作为生成器函数( Iterable[YieldType]
)的返回类型注解,因为这让 mypy 知道用户能够调用函数返回对象的 next()
方法。不过,要记住,如果您认为 next() 可以被调用是实现细节,那么使用 Iterable 可能是更好的选择。
如果您希望生成器通过 send()
方法接受值或返回值,则应使用 Generator[YieldType, SendType, ReturnType]
泛型类型,而不是 Iterator 或 Iterable。例如:
def echo_round() -> Generator[int, float, str]:
sent = yield 0
while sent >= 0:
sent = yield round(sent)
return 'Done'
注意,与 typing 模块中的许多其他泛型不同, Generator
的 SendType 是协变的,而不是协变或不变的。
如果您不打算接收或返回值,则应相应地将 SendType 或 ReturnType 设置为 None。例如,我们可以将第一个示例注解为:
def squares(n: int) -> Generator[int, None, None]:
for i in range(n):
yield i * i
这与使用 Iterator[int]
或 Iterable[int]
略有不同,因为生成器具有 close()
、 send()
和 throw()
方法,而通用的迭代器和可迭代对象则没有。如果您计划在返回的生成器上调用这些方法,请使用 Generator
类型,而不是 Iterator
或 Iterable
。