Python 的动态性意味着,尽管支持不断扩展,但类型提示永远不会覆盖所有情况。 对于边缘情况,我们需要使用 "逃生舱 "来覆盖类型检查器。
# type: ignore 注释就是这样的一个避风港,它可以禁止给定行的类型检查错误。我以前讲过管理这样的注释,并使它们更具体,以便它们能够做到。
另一个我们可以使用的最好的逃生舱门是铸造。 我们可以明确地用 typing.cast(),或者用类型提示隐含地从Any 。通过铸造,我们可以强迫类型检查器将一个变量视为一个给定的类型。
让我们来看看我们如何使用显式和隐式铸造,以及Mypy管理对cast() 的调用的功能。
最简单的cast()
当我们调用cast() ,我们传递给它两个参数:一个类型和一个值。cast() 返回value ,没有变化,但是类型检查器会把返回值当作给定的类型,而不是输入类型。例如,我们可以让Mypy把一个整数当作一个字符串。
from __future__ import annotations
from typing import cast
x = 1
reveal_type(x)
y = cast(str, x)
reveal_type(y)
y.upper()
用Mypy检查这个程序,它没有报告任何错误,但是它为我们调试了x 和y 的类型。
$ mypy example.py
example.py:6: note: Revealed type is "builtins.int"
example.py:8: note: Revealed type is "builtins.str"
但是,如果我们删除reveal_type() 的调用并运行该代码,它就会崩溃。
$ python example.py
Traceback (most recent call last):
File "/.../example.py", line 7, in <module>
y.upper()
AttributeError: 'int' object has no attribute 'upper'
通常Mypy会发现这个错误,因为它知道int 对象没有upper() 方法。但是我们的cast() 迫使Mypy把y 作为str ,所以它认为这个调用会成功。
在运行时,cast() 其实什么也没做--它的特殊行为只在于类型检查器如何解释它。Python 的源代码显示cast() 是一个简单的无操作函数调用。
def cast(typ, val):
"""Cast a value to a type.
This returns the value unchanged. To the type checker this
signals that the return value has the designated type, but at
runtime we intentionally don't check anything (we want this
to be as fast as possible).
"""
return val
由于cast() 是一个普通的 Python 函数,调用它确实会带来非常轻微的运行时性能损失。这很少会成为一个问题。 像往常一样,在对性能做任何假设之前,你应该对你的代码进行剖析。
使用案例
使用cast() 的主要情况是当一个模块的类型提示丢失、不完整或不正确时。这可能是第三方软件包的情况,或者偶尔是标准库中的东西。
以此为例。
import datetime as dt
from typing import cast
from third_party import get_data
data = get_data()
last_import_time = cast(dt.datetime, data["last_import_time"])
想象一下get_data() 的返回类型是dict[str, Any] ,而不是使用更严格的按键类型的 TypedDict 。通过阅读文档或源代码,我们可能发现"last_import_time" 的键总是包含一个datetime 对象。因此,当我们访问它时,我们可以用cast() 来包装它,告诉我们的类型检查器真正的类型,而不是继续用Any 。
当我们遇到缺失的、不完整的或不正确的类型提示时,我们可以回馈一个修正。 这可能是在包本身,它的相关存根包,或在Python 的 typeshed 中的单独存根。 但在这样的修正发布之前,我们需要使用cast() 来使我们的代码通过类型检查。
隐式投射来自Any
值得注意的是,Any 有特殊的待遇:当我们将一个类型为Any 的变量存储在一个特定类型的变量中时,类型检查器会将其视为隐式投射。因此我们可以不使用cast() 来写我们之前的例子。
import datetime as dt
from third_party import get_data
data = get_data()
last_import_time: dt.datetime = data["last_import_time"]
当我们与返回Any 的库交互时,这种隐式转换是我们应该使用的第一个工具。当我们将一个类型为Any 的变量作为一个特定类型的函数参数或返回值传递时,它也适用。
在处理除Any 以外的不正确类型时,直接调用cast() 往往更有用。
Mypy的warn_redundant_casts 选项
当我们使用cast() 来覆盖一个第三方函数的类型时,这个类型会在以后的版本中被纠正(也许是来自我们自己的PR!)。在这样的更新之后,cast() 是不必要的杂乱,可能会使读者感到困惑。
我们可以通过激活Mypy的warn_redundant_casts 选项来检测这种不必要的转换。打开这个标志后,Mypy将为每次使用cast() ,将变量转换为它已经拥有的类型而记录一个错误。
(这提供了一个类似于warn_unused_ignores 的类型清洁检查,我在前面讲过)。
例如,以这个不必要的cast() 。
from typing import cast
x = 1
y = cast(int, x)
在激活该选项的情况下运行Mypy,我们看到这个错误。
$ mypy --warn-redundant-casts example.py
example.py:6: error: Redundant cast to "int"
Found 1 error in 1 file (checked 1 source file)
激活这个选项对于保持我们的类型清洁是一个很好的保护。