介绍

Python 是一种动态类型语言。它在运行时而不是编译时确定数据类型。Python 类型的一些示例包括整数、浮点数、字符串和布尔值。动态类型语言与静态类型语言(如 C++、Java 和 Fortran)形成对比,后者在编译时执行类型检查。

动态类型语言(如 Python)的一个优点是程序员不需要为每个声明的变量指定类型。相反,Python 解释器在运行时推断并分配一个类型。与使用静态类型语言(如 Java)相比,这导致更简洁的代码可以更快地编写。

这种简洁的风格也有其缺点。因为解释器更努力地填充 Python 留下的隐含内容,所以 Python 程序可能需要更长的时间来执行。你也可能偶尔会遇到错误,因为 Python 错误地解释了变量的类型。代码完成工具也可以更好地工作,并且对于静态类型的语言功能更全面。

最近对 Python 的增强使静态类型成为一种选择。替代语法现在让程序员可以选择以静态类型的方式编写他们的 Python 代码。Mypy是一个用于帮助您使用类型注释编写或重写 Python 代码的工具。这个工具为你的 Python 程序带来了静态类型的好处。

什么是Mypy?

Mypy 是一个用于静态类型检查 Python 代码的工具。Python 的创始人 Guido van Rossum已经在 Mypy 上工作了几年。Mypy 对静态类型 Python 的验证可以使程序更加正确、可读、可重构和可测试。如果你想使用 Python,并且想要静态类型的优势,那么可以考虑使用 Mypy。存在Pyre 等 Mypy的替代品,但 Mypy 目前在 Python 社区中更受欢迎。

笔记
静态类型语言以更难学习而著称。将现有 Python 代码转换为静态类型代码可能会令人生畏,因为可能需要更改许多代码行。本指南说明了如何调整现有 Python 项目以增量使用 Mypy 和静态类型。

如何安装和使用 Mypy

如何安装 Mypy

使用 Pip 和以下命令在您的系统上安装 Mypy:

1
python -m pip install mypy

Mypy 基本用法

成功安装 Mypy 后,将目录更改为包含现有 Python 源文件的目录,然后使用以下命令运行 Mypy:

1
mypy *.py

如果没有发现错误,您应该会看到类似的输出。

Success: no issues found in N source files

如果您没有立即可用的 Python 源文件,请为此示例创建一个。

1
2
cd /tmp
echo "print('Hello, world.')" > test1.py

使用以下命令运行 Mypy:

1
mypy *.py

返回以下输出:

Success: no issues found in 1 source file

默认配置不提供有关静态类型的任何有用信息。这是因为 Python 示例没有定义任何静态类型。

使用 Mypy 识别错误

print Mypy 可以帮助在开发生命周期的早期识别错误,例如语句中缺少括号。与 Python 2 相比,Python 3 严格要求print语句周围有括号。如果您正在努力将 Python 2 程序更新到 Python 3,Mypy 可以帮助您识别常见的语法错误,例如缺少括号。

创建一个示例 Python 文件并运行 Mypy 以查看其错误处理的实际效果。

1
2
echo "print 'Hello, world.'" > test2.py
mypy *.py

Mypy 返回以下错误:

1
2
error: Missing parentheses in call to 'print'. Did you mean print('Hello, world.')?
Found 1 error in 1 file (errors prevented further checking)

Mypy 可以print在初始运行时识别每个需要括号的语句。

使用类型注解的静态类型
Mypy 允许您向函数添加类型注释,以帮助它检测与不正确的函数返回类型相关的错误。考虑以下示例:

File: test3.py

1
2
3
4
def legal_name(first: str, last:str) -> str:
return 'My legal name is:' + first + ' '+ last

legal_name('Jane', 5)

运行mypy test3.py时,会看到以下错误消息:

1
2
test3.py:4: error: Argument 2 to "legal_name" has incompatible type "int"; expected "str"
Found 1 error in 1 file (checked 1 source file)

第一行 def legal_name(first: str, last:str) -> str: 指定函数 legal_name() 需要string类型的参数并返回string类型的值。 Mypy 能够检测到函数调用的第二个参数不满足类型注释要求。

如果没有类型注释,Mypy 不会检测到 int 类型参数的任何问题。

NOTE:
使用 mypy 的 --disallow-untyped-defs 命令行选项,对所有函数定义强制执行静态类型。 如果 Python 项目不使用类型注释的第三方库,则此选项可能过于严格。

Mypy可以识别Python程序中所有对象上的类型注释。对于本指南,重点是函数签名,而不是Python程序中的所有其他对象。在开始使用Mypy时,请关注Python代码的函数定义。

当使用类型注释重构Python代码时,首先要注释所有函数定义。接下来,可以考虑将类型注释添加到不仅包含在函数签名中的变量中。

一些开发人员认为Mypy的大部分好处来自于向函数声明添加类型注释。对其他变量进行更详尽的注释可能需要付出更多的努力。

类型别名和定义

类型注释的大部分功能来自特定于域的类型定义,即超出内置类型的类型定义。考虑以下示例:

1
2
3
def retrieve(url):
"""Retrieve the content found at url."""
...

简单的类型注释将上面的示例改进为以下内容:

1
2
3
4
5
URL = str

def retrieve(url: URL) -> str:
"""Retrieve the content found at url."""
...

URL是一个类型别名,它比bare str更清楚地表达了变量URL的意图。检索定义不接受url参数的字符串。url参数的类型必须为url。正确的URL符合特定的文档化语法

类型别名的另一个好处是复杂类型的缩写。在下面的函数定义中可以看到这种优势的一个例子。

1
2
3
def compose(first: list[dict[str, float]], second: list[dict[str, float]]) -> list[dict[str, float]]
...

上面的函数定义可以写成如下图所示:

1
2
3
4
MyType = list[dict[str, float]]
...
def compose(first: MyType, second: MyType) -> MyType:
...

与类型别名一样有价值,Python 有一个替代方法,即类型定义,它也很强大。下面的代码确保您的 url 参数使用正确的 URL 语法:

1
2
3
4
5
6
from typing import NewType
...
URL = NewType("URL", str)
...
def retrieve(url: URL) -> str:
...

有了这个类型定义,Mypy 拒绝方法调用,例如retrieve(“not a true URL”),而它接受retrieve(URL(“https://www.linode.com”))。 Python 程序员习惯于在运行时检查 URL 等特殊语法。 Mypy 带来了将这些表达为强大的编译时验证的机会。

除了本指南介绍的内容之外,还有更多用于类型定义的工具。 即使没有这些高级工具,您也可以使用如上所示的类型别名和类型定义来使您自己的源代码更具表现力。

有了这个类型定义,Mypy 拒绝方法调用,例如retrieve("not a true URL"),而它接受retrieve(URL("https://www.linode.com"))

Python 程序员习惯于在运行时检查 URL 等特殊语法。 Mypy 带来了将这些表达为强大的编译时验证的机会。

除了介绍的内容之外,还有更多用于类型定义的工具。 即使没有这些高级工具,也可以使用如上所示的类型别名和类型定义来使您自己的源代码更具表现力。

指令

Mypy 的指令会调整它返回的信息。 考虑以下示例:

  1. 创建一个名为 test2.py 的文件,其内容如下:

File: test2.py

1
2
3
4
5
6
7
8
9
10
11
12
def f1(error: str) -> int:
"""While this does nothing particularly useful, the
syntax is typical of Python code often found "in the
wild"."""
if len(error) > 4:
return 3
if len(error) > 10:
return 1 + error
return 99

print(f"The return value is {f1('abc')}.")
print(f"The return value is {f1('abcef')}.")
  1. test2.py 文件上运行 Mypy:
1
mypy --disallow-untyped-defs test2.py

Mypy 报告 test2.py 的函数定义为完全注释。 f1() 函数的每个 if 子句都返回预期的整数类型。 但是,Mypy 还是报错:

1
test2.py:8: error: Unsupported operand types for + ("int" and "str")
  1. 重新运行 Mypy 以显示错误代码:
1
mypy --show-error-codes --disallow-untyped-defs test2.py

错误信息变得更具描述性:

1
test2.py:8: error: Unsupported operand types for + ("int" and "str")  [operator]
  1. 将上例第 8 行的 return 语句更新为:
1
2
3
4
...
return 1 + error: # type: ignore[operator]
...

重新运行Mypy时,它不会报告任何错误。#type:ignore[operator]指令将其标记为问题,并最终需要解决方案。

该指令是一条注释,它使程序的行为完全保持不变。现在,当重新运行Mypy时,会收到一条成功消息,可以继续处理代码的其他方面。这是一种你可以更普遍地使用的策略。

在对大量源代码注释类型时,建议只选择一个错误代码,清除一个错误类型的所有出现,同时使用指令暂时忽略其他问题,并进行迭代。

Mypy配置

可以使用名为 mypy.ini 的配置文件来配置 Mypy。

在项目目录中创建一个名为 mypy.ini 的新文件。 将以下内容添加到文件中:

1
disallow_untyped_defs = true

在该目录中启动的任何 mypy 命令的行为就像使用命令行参数 --disallow-untyped-defs 运行一样。

Mypy 的配置文件可以有不同的文件名。 从 Mypy 开始时,mypy.ini 是一个不错的选择,并且默认情况下 Mypy 可以识别。 还可以使用 .toml 文件来存储 Mypy 配置。

目标配置至少应包括以下配置:

1
2
3
4
5
6
disallow_untyped_defs = true
no_implicit_optional = true
show_error_codes = true
strict_equality = true
warn_redundant_casts = true
warn_unused_ignores = true

此配置可帮助采用增量方法来重构本指南中推荐的非类型化 Python 代码。

这种组合为项目带来了 Mypy 的大部分好处,而不涉及 Mypy 更困难的方面。

结论

Mypy 的默认值值得学习。 例如,不注释方法定义的 self 参数是惯用的。 一个常见的构造函数是:

1
def __init__(self, *args: str, **kwargs: str) -> None

即使设置了 disallow_untyped_defs,Mypy 也可以识别如何正确处理 self 而无需程序员的显式注释。

类型注释是Python编码的一个重大变化,它带来了显著的好处。使用本文中概述的技术将Python项目迁移到类型注释代码。

参考

https://mypy.readthedocs.io/en/stable/running_mypy.html