Python 类型检查
BG
前几天,Henry 安排我调研一下 Python 3 的 typing 和 annotations,他说这是很有用的 feature,尤其是对软件开发的工作而言。我突然发现 LeetCode 平台的 Solution 其实就用了 typing check,于是在这里给大家分享一下这方面的知识。
传统上,Python 解释器以灵活但隐式的方式处理类型。Python 最新的几个版本允许您指定明确的类型进行提示,有些工具可以使用这些提示来帮助您更有效地开发代码。那么最新的几个版本都做了什么呢?Python 3.0 开始支持函数标注,3.5、3.6 开始支持变量标注。从 3.7 开始,from __future__ import annotation
已经可以添加 Python 4 风格的纯文本标注了,相信未来 Python4 强大的类型检查功能一定能成为这门语言的巨大优势。
这篇文章主要涉及以下几个方面:
- 类型标注和类型注释是什么?
- 如何为代码添加静态类型检查?
- 怎样运行静态类型检查工具?
- 如何在运行时强制类型转换?
动态和静态类型检查
Python 是一个动态类型检查的语言,以灵活但隐式的方式处理类型。Python 解释器仅仅在运行时检查类型是否正确,并且允许在运行时改变变量类型。
>>> if False:
... 1 + "two" # This line never runs, so no TypeError is raised
... else:
... 1 + 2
...
3
>>> 1 + "two" # Now this is type checked, and a TypeError is raised
TypeError: unsupported operand type(s) for +: 'int' and 'str'
Python 最新的几个版本允许您指定明确的类型进行提示,有些工具可以使用这些提示来帮助您更有效地开发代码。
类型提示
既然 Python 是动态类型检查的语言,不可避免有很多隐式的类型转换,如何确保它们按计划运行呢?Python 的类型检查系统主要是用类型标注和类型注释进行类型提示和检查。
类型提示简例
我们首先通过一个例子来简要说明。假如我们要向函数中添加关于类型的信息,首先需要按如下方式对它的参数和返回值设置类型标注:
# headlines.py
def headline(text: str, align: bool = True) -> str:
if align:
return f"{text.title()}\n{'-' * len(text)}"
else:
return f" {text.title()} ".center(50, "o")
print(headline("python type checking"))
print(headline("use mypy", centered=True))
但是这样添加类型提示没有运行时的效果——如果我们用错误类型的 align
参数,程序依然可以在不报错、不警告的情况下正常运行。
$ python headlines.py
Python Type Checking
--------------------
oooooooooooooooooooo Use Mypy oooooooooooooooooooo
因此,我们需要静态检查工具来排除这类错误(例如 PyCharm 中就包含这种检查)。最常用的静态类型检查工具是 Mypy。
$ pip install mypy
Successfully installed mypy.
$ mypy headlines.py
Success: no issues found in 1 source file
如果没有报错,说明类型检查通过;否则,会提示出问题的地方。值得注意的是,类型检查可以向下(subtype not subclass)兼容,比如整数就可以在 Mypy 中通过浮点数类型标注的检查(int 是 double 的 subtype,但不是其 subclass)。
这种检查对于写出可读性较好的代码是十分有帮助的——Bernát Gábor 曾在他的 The State of Type Hints in Python 中说过,“类型提示应当出现在任何值得单元测试的代码里”。
接下来我们将会更详细地介绍 Python 的类型检查系统:如何运行静态类型检查工具(主要是 Mypy)、如何检查引用了不含类型提示的静态库的代码、如何在运行时检查类型标注。
类型标注
类型标注是自 Python 3.0 开始引入的特征,是添加类型提示的重要方法。例如这段代码就引入了类型标注,你可以通过调用 circumference.__annotations__
来查看函数中所有的类型标注。
import math
def circumference(radius: float) -> float:
return 2 * math.pi * radius
当然,除了函数函数,变量也是可以类型标注的,你可以通过调用 __annotations__
来查看函数中所有的类型标注。
pi: float = 3.142
def circumference(radius: float) -> float:
return 2 * pi * radius
变量类型标注赋予了 Python 静态语言的性质,即声明与赋值分离:
>>> nothing: str
>>> nothing
NameError: name 'nothing' is not defined
>>> __annotations__
{'nothing': <class 'str'>}
类型注释
如上所述,Python 的类型标注是 3.0 之后才支持的,这说明如果你需要编写支持遗留Python 的代码,就不能使用标注。为了应对这个问题,你可以尝试使用类型注释——一种特殊格式的代码注释——作为你代码的类型提示。
import math
pi = 3.142 # type: float
def circumference(radius):
# type: (float) -> float
return 2 * pi * radius
def headline(text, width=80, fill_char="-"):
# type: (str, int, str) -> str
return f" {text.title()} ".center(width, fill_char)
def headline(
text, # type: str
width=80, # type: int
fill_char="-", # type: str
): # type: (...) -> str
return f" {text.title()} ".center(width, fill_char)
print(headline("type comments work", width=40))
这种注释不包含在类型标注中,你无法通过 __annotations__
找到它,同类型标注一样,你仍然可以通过 Mypy 运行得到类型检查结果。
类型别名
当然 Python 有更强大的类型提示功能,支持复杂的类型提示,例如 List[Tuple[str, str]]
。我们可以为这些类型制作别名来让代码可读性更好,Card = Tuple[str, str]
。
特殊函数返回类型
- 空返回:如果没有显式定义返回什么的话,默认返回的是
None
类型; - 不返回:如果一个函数上来就抛出了异常,那么它就不应该被返回,可以引用
typing.NoReturn
; - 任意类型:可以使用
Any
,不过一般不常用,因为一旦使用可能会丢失类型信息。
更多资源
使用类型提示可以让您更容易地推断代码、发现细微的错误并维护干净的体系结构。对于 OIer 来说,掌握 Python 类型检查系统的基本操作就足够了,项目实操中,如果你想写出风格更好的、易于类型检查的代码,你可以参考 Mypy 的文档。