类型收缩¶
本节专门介绍 mypy 支持的几种类型收缩技术。
类型缩小是指您让类型检查器相信更广泛的类型实际上更具体,例如,Shape
类型的对象实际上是更窄的 Square
类型。
Type narrowing
This section is dedicated to several type narrowing techniques which are supported by mypy.
Type narrowing is when you convince a type checker that a broader type is actually more specific, for instance, that an object of type Shape
is actually of the narrower type Square
.
类型收缩表达式¶
Type narrowing expressions
缩小类型范围的最简单方法是使用受支持的表达式之一:
isinstance
就像isinstance(obj, float)
会将obj
缩小为float
类型issubclass
就像issubclass(cls, MyClass)
会将cls
缩小为Type[MyClass]
类型type
就像type(obj) is int
会将obj
缩小为int
类型callable
就像callable(obj)
将对象缩小为可调用类型
类型缩小是根据上下文而定的。 例如,根据条件,mypy 将仅在if
分支内缩小表达式:
def function(arg: object):
if isinstance(arg, int):
# 类型仅在“if”分支内缩小
reveal_type(arg) # 显示类型:“builtins.int”
elif isinstance(arg, str) or isinstance(arg, bool):
# 在这个 elif 分支中,类型以不同的方式缩小:
reveal_type(arg) # 显示类型:“builtins.str |builtins.bool”
# 后续的缩小操作将进一步缩小类型
if isinstance(arg, bool):
reveal_type(arg) # 显示类型: "builtins.bool"
# 回到“if”语句之外,类型没有缩小:
reveal_type(arg) # 显示类型: "builtins.object"
Mypy 理解return
或异常引发对对象类型的影响:
def function(arg: int | str):
if isinstance(arg, int):
return
# 此时 `arg` 不能是 `int`:
reveal_type(arg) # 显示类型: "builtins.str"
我们还可以使用assert
来缩小相同上下文中的类型:
def function(arg: Any):
assert isinstance(arg, int)
reveal_type(arg) # 显示类型: "builtins.int"
Note
使用 --warn-unreachable
将类型缩小到某些不可能的状态将被视为错误。
def function(arg: int):
# error: “int”和“str”的子类不能存在:
# 会有不兼容的方法签名
assert isinstance(arg, str)
# error: 声明无法访问(Statement is unreachable)
print("so mypy concludes the assert will always trigger")
如果没有--warn-unreachable
,mypy 将不会检查它认为无法访问的代码。 有关更多信息,请参阅无法访问的代码
。
x: int = 1
assert isinstance(x, str)
reveal_type(x) # 显示类型 "builtins.int"
print(x + '!') # 使用`mypy`进行类型检查,但在运行时失败。
The simplest way to narrow a type is to use one of the supported expressions:
isinstance
like inisinstance(obj, float)
will narrowobj
to havefloat
typeissubclass
like inissubclass(cls, MyClass)
will narrowcls
to beType[MyClass]
type
like intype(obj) is int
will narrowobj
to haveint
typecallable
like incallable(obj)
will narrow object to callable type
Type narrowing is contextual. For example, based on the condition, mypy will narrow an expression only within an if
branch:
def function(arg: object):
if isinstance(arg, int):
# Type is narrowed within the ``if`` branch only
reveal_type(arg) # Revealed type: "builtins.int"
elif isinstance(arg, str) or isinstance(arg, bool):
# Type is narrowed differently within this ``elif`` branch:
reveal_type(arg) # Revealed type: "builtins.str | builtins.bool"
# Subsequent narrowing operations will narrow the type further
if isinstance(arg, bool):
reveal_type(arg) # Revealed type: "builtins.bool"
# Back outside of the ``if`` statement, the type isn't narrowed:
reveal_type(arg) # Revealed type: "builtins.object"
Mypy understands the implications return
or exception raising can have
for what type an object could be:
def function(arg: int | str):
if isinstance(arg, int):
return
# `arg` can't be `int` at this point:
reveal_type(arg) # Revealed type: "builtins.str"
We can also use assert
to narrow types in the same context:
def function(arg: Any):
assert isinstance(arg, int)
reveal_type(arg) # Revealed type: "builtins.int"
Note
With --warn-unreachable
narrowing types to some impossible state will be treated as an error.
def function(arg: int):
# error: Subclass of "int" and "str" cannot exist:
# would have incompatible method signatures
assert isinstance(arg, str)
# error: Statement is unreachable
print("so mypy concludes the assert will always trigger")
Without --warn-unreachable
mypy will simply not check code it deems to be unreachable. See unreachable code
for more information.
x: int = 1
assert isinstance(x, str)
reveal_type(x) # Revealed type is "builtins.int"
print(x + '!') # Typechecks with `mypy`, but fails in runtime.
是否为子类¶
issubclass
Mypy 还可以使用 issubclass
在处理类型和元类时进行更好的类型推断:
class MyCalcMeta(type):
@classmethod
def calc(cls) -> int:
...
def f(o: object) -> None:
t = type(o) # 我们必须在这里使用一个变量
reveal_type(t) # 揭示的类型是 "builtins.type"
if issubclass(t, MyCalcMeta): # `issubclass(type(o), MyCalcMeta)` 不起作用
reveal_type(t) # 揭示的类型是 "Type[MyCalcMeta]"
t.calc() # Okay
Mypy can also use issubclass
for better type inference when working with types and metaclasses:
class MyCalcMeta(type):
@classmethod
def calc(cls) -> int:
...
def f(o: object) -> None:
t = type(o) # We must use a variable here
reveal_type(t) # Revealed type is "builtins.type"
if issubclass(t, MyCalcMeta): # `issubclass(type(o), MyCalcMeta)` won't work
reveal_type(t) # Revealed type is "Type[MyCalcMeta]"
t.calc() # Okay
是否可调用¶
callable
Mypy 在类型检查期间知道哪些类型可调用,哪些类型不可调用。 所以,我们知道 callable()
会返回什么。 例如:
from typing import Callable
x: Callable[[], int]
if callable(x):
reveal_type(x) # N: 揭示的类型是 "def () -> builtins.int"
else:
... # 永远不会被执行并且会引发错误 `--warn-unreachable`
callable
函数甚至可以将 Union
类型拆分为可调用部分和不可调用部分:
from typing import Callable, Union
x: Union[int, Callable[[], int]]
if callable(x):
reveal_type(x) # N: 揭示的类型是 "def () -> builtins.int"
else:
reveal_type(x) # N: 揭示的类型是 "builtins.int"
Mypy knows what types are callable and which ones are not during type checking. So, we know what callable()
will return. For example:
from typing import Callable
x: Callable[[], int]
if callable(x):
reveal_type(x) # N: Revealed type is "def () -> builtins.int"
else:
... # Will never be executed and will raise error with `--warn-unreachable`
callable
function can even split Union
type for callable and non-callable parts:
from typing import Callable, Union
x: Union[int, Callable[[], int]]
if callable(x):
reveal_type(x) # N: Revealed type is "def () -> builtins.int"
else:
reveal_type(x) # N: Revealed type is "builtins.int"
转换¶
Casts
Mypy 支持类型转换,通常用于将静态类型值强制转换为子类型。 然而,与 Java 或 C# 等语言不同,mypy 强制转换仅用作类型检查器的提示,并且不执行运行时类型检查。 使用函数 typing.cast
执行转换:
from typing import cast
o: object = [1]
x = cast(list[int], o) # OK
y = cast(list[str], o) # OK (强制转换不执行实际的运行时检查)
为了支持上述类型转换的运行时检查,我们必须检查所有列表项的类型,这对于大型列表来说效率非常低。 强制转换用于消除虚假的类型检查器警告,并在类型检查器无法完全理解正在发生的情况时为类型检查器提供一些帮助。
Note
You can use an assertion if you want to perform an actual runtime check:
def foo(o: object) -> None:
print(o + 5) # Error: can't add 'object' and 'int'
assert isinstance(o, int)
print(o + 5) # OK: type of 'o' is 'int' here
正如前面所解释的,您不需要对Any
类型的表达式进行强制转换,或者在分配给类型为Any
的变量时进行强制转换。 您还可以使用Any
作为转换目标类型——这允许您对结果执行任何操作。 例如:
from typing import cast, Any
x = 1
x.whatever() # Type check error
y = cast(Any, x)
y.whatever() # Type check OK (runtime error)
Mypy supports type casts that are usually used to coerce a statically typed value to a subtype. Unlike languages such as Java or C#, however, mypy casts are only used as hints for the type checker, and they don't perform a runtime type check. Use the function typing.cast
to perform a cast:
from typing import cast
o: object = [1]
x = cast(list[int], o) # OK
y = cast(list[str], o) # OK (cast performs no actual runtime check)
To support runtime checking of casts such as the above, we'd have to check the types of all list items, which would be very inefficient for large lists. Casts are used to silence spurious type checker warnings and give the type checker a little help when it can't quite understand what is going on.
Note
You can use an assertion if you want to perform an actual runtime check:
def foo(o: object) -> None:
print(o + 5) # Error: can't add 'object' and 'int'
assert isinstance(o, int)
print(o + 5) # OK: type of 'o' is 'int' here
You don't need a cast for expressions with type Any
, or when assigning to a variable with type Any
, as was explained earlier. You can also use Any
as the cast target type -- this lets you perform any operations on the result. For example:
from typing import cast, Any
x = 1
x.whatever() # Type check error
y = cast(Any, x)
y.whatever() # Type check OK (runtime error)
用户定义的类型保护¶
User-Defined Type Guards
Mypy 支持用户定义的类型防护 (PEP 647
).
类型保护是程序影响类型检查器基于运行时检查所采用的条件类型缩小的一种方式。
基本上,TypeGuard
是bool
类型的“智能(smart)”别名。 让我们看一下常规的bool
示例:
def is_str_list(val: list[object]) -> bool:
"""判断列表中的所有对象是否都是字符串"""
return all(isinstance(x, str) for x in val)
def func1(val: list[object]) -> None:
if is_str_list(val):
reveal_type(val) # Reveals list[object]
print(" ".join(val)) # Error: incompatible type
与TypeGuard
相同的示例:
from typing import TypeGuard # use `typing_extensions` for Python 3.9 and below
def is_str_list(val: list[object]) -> TypeGuard[list[str]]:
"""判断列表中的所有对象是否都是字符串"""
return all(isinstance(x, str) for x in val)
def func1(val: list[object]) -> None:
if is_str_list(val):
reveal_type(val) # list[str]
print(" ".join(val)) # ok
它是如何工作的? TypeGuard
将第一个函数参数 (val
) 缩小为指定为第一个类型参数 (list[str]
) 的类型。
Note
缩小范围不严格(not strict)。 例如,您可以将str
缩小为int
:
def f(value: str) -> TypeGuard[int]:
return True
注意: 由于不强制执行严格的缩小,因此很容易破坏类型安全。
然而,坚定或不知情的开发人员可以通过多种方式破坏类型安全——最常见的是使用强制转换或 Any。 如果 Python 开发人员花时间在代码中学习和实现用户定义的类型保护,则可以安全地假设他们对类型安全感兴趣,并且不会以破坏类型安全或破坏类型安全的方式编写类型保护函数。 产生无意义的结果。
Mypy supports User-Defined Type Guards (PEP 647
).
A type guard is a way for programs to influence conditional type narrowing employed by a type checker based on runtime checks.
Basically, a TypeGuard
is a "smart" alias for a bool
type. Let's have a look at the regular bool
example:
def is_str_list(val: list[object]) -> bool:
"""Determines whether all objects in the list are strings"""
return all(isinstance(x, str) for x in val)
def func1(val: list[object]) -> None:
if is_str_list(val):
reveal_type(val) # Reveals list[object]
print(" ".join(val)) # Error: incompatible type
The same example with TypeGuard
:
from typing import TypeGuard # use `typing_extensions` for Python 3.9 and below
def is_str_list(val: list[object]) -> TypeGuard[list[str]]:
"""Determines whether all objects in the list are strings"""
return all(isinstance(x, str) for x in val)
def func1(val: list[object]) -> None:
if is_str_list(val):
reveal_type(val) # list[str]
print(" ".join(val)) # ok
How does it work? TypeGuard
narrows the first function argument (val
) to the type specified as the first type parameter (list[str]
).
Note
Narrowing is not strict. For example, you can narrow str
to int
:
def f(value: str) -> TypeGuard[int]:
return True
Note: since strict narrowing is not enforced, it's easy to break type safety.
However, there are many ways a determined or uninformed developer can subvert type safety -- most commonly by using cast or Any. If a Python developer takes the time to learn about and implement user-defined type guards within their code, it is safe to assume that they are interested in type safety and will not write their type guard functions in a way that will undermine type safety or produce nonsensical results.
泛型类型保护¶
TypeGuard
也可以使用泛型类型:
from typing import TypeVar
from typing import TypeGuard # use `typing_extensions` for `python<3.10`
_T = TypeVar("_T")
def is_two_element_tuple(val: tuple[_T, ...]) -> TypeGuard[tuple[_T, _T]]:
return len(val) == 2
def func(names: tuple[str, ...]):
if is_two_element_tuple(names):
reveal_type(names) # tuple[str, str]
else:
reveal_type(names) # tuple[str, ...]
TypeGuard
can also work with generic types:
from typing import TypeVar
from typing import TypeGuard # use `typing_extensions` for `python<3.10`
_T = TypeVar("_T")
def is_two_element_tuple(val: tuple[_T, ...]) -> TypeGuard[tuple[_T, _T]]:
return len(val) == 2
def func(names: tuple[str, ...]):
if is_two_element_tuple(names):
reveal_type(names) # tuple[str, str]
else:
reveal_type(names) # tuple[str, ...]
带参数的类型保护¶
Typeguards with parameters
类型保护函数可以接受额外的参数:
from typing import Type, TypeVar
from typing import TypeGuard # use `typing_extensions` for `python<3.10`
_T = TypeVar("_T")
def is_set_of(val: set[Any], type: Type[_T]) -> TypeGuard[set[_T]]:
return all(isinstance(x, type) for x in val)
items: set[Any]
if is_set_of(items, str):
reveal_type(items) # set[str]
Type guard functions can accept extra arguments:
from typing import Type, TypeVar
from typing import TypeGuard # use `typing_extensions` for `python<3.10`
_T = TypeVar("_T")
def is_set_of(val: set[Any], type: Type[_T]) -> TypeGuard[set[_T]]:
return all(isinstance(x, type) for x in val)
items: set[Any]
if is_set_of(items, str):
reveal_type(items) # set[str]
作为方法的类型保护¶
TypeGuards as methods
方法也可以充当
TypeGuard
:
class StrValidator:
def is_valid(self, instance: object) -> TypeGuard[str]:
return isinstance(instance, str)
def func(to_validate: object) -> None:
if StrValidator().is_valid(to_validate):
reveal_type(to_validate) # Revealed type is "builtins.str"
Note
请注意,TypeGuard
不会缩小self
的类型 或cls
隐式参数。
如果需要缩小self
或cls
,则可以将该值作为显式参数传递给类型保护函数:
class Parent:
def method(self) -> None:
reveal_type(self) # Revealed type is "Parent"
if is_child(self):
reveal_type(self) # Revealed type is "Child"
class Child(Parent):
...
def is_child(instance: Parent) -> TypeGuard[Child]:
return isinstance(instance, Child)
A method can also serve as the
TypeGuard
:
class StrValidator:
def is_valid(self, instance: object) -> TypeGuard[str]:
return isinstance(instance, str)
def func(to_validate: object) -> None:
if StrValidator().is_valid(to_validate):
reveal_type(to_validate) # Revealed type is "builtins.str"
Note
Note, that TypeGuard
does not narrow types of self
or cls
implicit arguments.
If narrowing of self
or cls
is required, the value can be passed as an explicit argument to a type guard function:
class Parent:
def method(self) -> None:
reveal_type(self) # Revealed type is "Parent"
if is_child(self):
reveal_type(self) # Revealed type is "Child"
class Child(Parent):
...
def is_child(instance: Parent) -> TypeGuard[Child]:
return isinstance(instance, Child)
作为类型保护的赋值表达式¶
Assignment expressions as TypeGuards
有时您可能需要创建一个新变量并同时将其缩小为某种特定类型。 这可以通过将TypeGuard
与 := 运算符 一起使用来实现。
from typing import TypeGuard # use `typing_extensions` for `python<3.10`
def is_float(a: object) -> TypeGuard[float]:
return isinstance(a, float)
def main(a: object) -> None:
if is_float(x := a):
reveal_type(x) # N: Revealed type is 'builtins.float'
reveal_type(a) # N: Revealed type is 'builtins.object'
reveal_type(x) # N: Revealed type is 'builtins.object'
reveal_type(a) # N: Revealed type is 'builtins.object'
这里会发生什么?
- 我们创建一个新变量
x
并为其分配值a
- 我们在
x
上运行is_float()
类型保护 - 它将
x
缩小为if
上下文中的float
,并且不涉及a
Note
这同样适用于isinstance(x := a, float)
。
Sometimes you might need to create a new variable and narrow it to some specific type at the same time. This can be achieved by using TypeGuard
together with := operator.
from typing import TypeGuard # use `typing_extensions` for `python<3.10`
def is_float(a: object) -> TypeGuard[float]:
return isinstance(a, float)
def main(a: object) -> None:
if is_float(x := a):
reveal_type(x) # N: Revealed type is 'builtins.float'
reveal_type(a) # N: Revealed type is 'builtins.object'
reveal_type(x) # N: Revealed type is 'builtins.object'
reveal_type(a) # N: Revealed type is 'builtins.object'
What happens here?
- We create a new variable
x
and assign a value ofa
to it - We run
is_float()
type guard onx
- It narrows
x
to befloat
in theif
context and does not toucha
Note
The same will work with isinstance(x := a, float)
as well.
创建日期: 2023年7月6日