介绍 Mypy,Python 的实验性可选静态类型检查器
使用 mypy
改进您的代码并在错误发生之前识别它们。
多年来,我一直使用动态语言——Perl、Ruby 和 Python。我喜欢这些语言提供的灵活性和表达性。例如,我可以定义一个对数字求和的函数
def mysum(numbers):
total = 0
for one_number in numbers:
total += one_number
return total
上面的函数适用于任何返回数字的可迭代对象。因此,我可以在数字的列表、元组或集合上运行上述函数。我甚至可以在键都是数字的字典上运行它。非常棒,对吧?
是的,但是对于习惯了静态编译语言的学生来说,这很难适应。毕竟,如何确保没有人向您传递字符串或数字字符串?如果您得到一个列表,其中某些但并非全部元素是数字,该怎么办?
多年来,我过去常常对这种担忧不屑一顾。毕竟,动态语言已经存在很长时间了,而且它们做得很好。而且真的,如果人们遇到这种类型不匹配的错误,那么也许他们应该更加关注。另外,如果您有足够的测试,您可能会没事的。
但是随着 Python(和其他动态语言)已经打入大型公司,我越来越相信静态类型检查是有道理的。特别是,许多 Python 新手正在从事大型项目,其中许多部分需要互操作,这一事实让我清楚地认识到,某种类型的类型检查可能是有用的。
您如何平衡这些需求?也就是说,您如何在享受 Python 作为动态类型语言的同时,获得一些额外的静态类型稳定感?
最流行的答案之一是称为 mypy
的系统,它利用 Python 3 的类型注解来实现其自身的目的。使用 mypy
意味着您可以像往常一样编写和运行 Python,随着时间的推移逐步添加静态类型检查,并在程序执行之外对其进行检查。
在本文中,我开始探索 mypy
以及如何使用它来检查程序中的问题。我对 mypy
印象深刻,我相信您很可能会在越来越多的地方看到它被部署,这在很大程度上是因为它是可选的,因此允许开发人员根据他们认为必要的程度使用它,并随着时间的推移逐步加强。
在 Python 中,用户不仅享受动态类型,还享受强类型。“动态”意味着变量没有类型,但值有类型。所以你可以说
>>> x = 100
>>> print(type(x))
int
>>> x = 'abcd'
>>> print(type(x))
str
>>> x = [10, 20, 30]
>>> print(type(x))
list
正如您所看到的,我可以运行上面的代码,并且它可以正常工作。它本身并不是特别有用,但在静态编译语言中,它甚至无法通过第一次编译。这是因为在这样的语言中,变量具有类型——这意味着如果您尝试将整数分配给字符串变量,您将收到错误。
相比之下,在动态语言中,变量根本没有类型。正如我上面所做的那样,运行 type
函数实际上并没有返回变量的类型,而是返回变量当前指向的数据类型。
仅仅因为一种语言是动态类型的,并不意味着它完全是松散的,让你可以为所欲为。(是的,这是一个技术术语。)例如,我可以尝试这样做
>>> x = 1
>>> y = '1'
>>> print(x+y)
该代码将导致错误,因为 Python 不知道如何将整数和字符串相加。它可以将两个整数相加(并获得整数结果)或两个字符串相加(并获得字符串结果),但不能将两者组合在一起。
您之前看到的 mysum
函数将 0 分配给局部变量“total”,然后将 numbers
的每个元素添加到其中。这意味着如果 numbers
包含任何非数字,您就会遇到麻烦。幸运的是,mypy
将能够为您解决这个问题。
Python 3 引入了“类型注解”的概念,从 Python 3.6 开始,您可以注解变量,而不仅仅是函数参数和返回值。这个想法是您可以在参数名称后放置一个冒号 (:),然后放置一个类型。例如
def hello(name:str):
return f'Hello, {name}'
在这里,我给 name
参数一个 str
的类型注解。如果您使用过静态类型语言,您可能会认为这将增加类型安全性。也就是说,您可能会认为如果我尝试执行
hello(5)
我会收到一个错误。但实际上,Python 会完全忽略这些类型注解。此外,您可以在注解中使用任何您想要的对象;虽然通常使用类型,但实际上您可以使用任何东西。
这可能会让您觉得完全荒谬。如果您永远不会使用它们,为什么要引入这样的注解?基本的想法是,编码工具和扩展程序将能够将注解用于他们自己的目的,包括(您稍后就会看到)用于类型检查的目的。
这很重要,所以我将重复并强调它:类型注解被 Python 语言忽略,尽管它确实将它们存储在一个名为 __annotations__
的属性中。例如,在定义上面的 hello
函数之后,您可以查看它的注解,这些注解存储为字典
>>> hello.__annotations__
{'name': <class 'str'>}
使用 Mypy
可以使用标准的 Python pip
包安装程序下载和安装 mypy
类型检查器。在我的系统中,在终端窗口中,我运行了
$ pip3 install -U mypy
pip3
反映了我正在使用 Python 3 而不是 Python 2。-U
选项表示我想升级我的 mypy
安装,如果自上次我在我的计算机上安装该软件包以来,该软件包已更新。如果您要全局且为所有用户安装此软件包,您可能需要使用 sudo
以 root 身份运行它。
一旦安装了 mypy
,您就可以运行它,并命名您的文件。例如,假设 hello.py 看起来像这样
def hello(name:str):
return f"Hello, {name}"
print(hello('world'))
print(hello(5))
print(hello([10, 20, 30]))
如果我运行这个程序,它实际上可以正常工作。但是我想使用该类型注解来确保我仅使用字符串参数调用该函数。因此,我可以在命令行上运行
$ mypy ./hello.py
我得到以下输出
hello.py:7: error: Argument 1 to "hello" has incompatible type
↪"int"; expected "str"
hello.py:8: error: Argument 1 to "hello" has incompatible type
↪"List[int]"; expected "str"
果然,mypy
已经识别出两个地方违反了我用类型注解表达的期望——即,只有字符串将作为参数传递给“hello”。这不会困扰 Python,但它应该困扰您,要么是因为类型注解需要放宽,要么是因为(就像在本例中一样),它正在使用错误类型的参数调用该函数。
换句话说,mypy
不会告诉您该怎么做或阻止您运行程序。但它会尝试给您警告,如果您将此与 Git hook 和/或集成和测试系统结合在一起,您将更好地了解您的程序可能在哪里出现问题。
当然,mypy
只会在有注解的地方进行检查。如果您未能注解某些内容,mypy
将无法检查它。
例如,我没有注解函数的返回值。我可以修复它,表明它返回一个字符串,使用
def hello(name:str) -> str:
return f"Hello, {name}"
请注意,Python 引入了一种新语法(->
箭头),并允许我在行尾冒号之前粘贴一个注解,以便注解能够工作。注解字典现在也已扩展
>>> hello.__annotations__
{'name': <class 'str'>, 'return': <class 'str'>}
如果您想知道,如果您有一个名为 return
的局部变量,它与返回值的注解冲突,Python 会怎么做......那么,“return”是一个保留字,不能用作参数名称。
让我们回到 mysum
函数。mypy
将能够(和不能)检查什么?例如,假设以下文件
def mysum(numbers:list) -> int:
output = 0
for one_number in numbers:
output += one_number
return output
print(mysum([10, 20, 30, 40, 50]))
print(mysum((10, 20, 30, 40, 50)))
print(mysum([10, 20, 'abc', 'def', 50]))
print(mysum('abcd'))
正如您所看到的,我已经注解了 numbers
参数以仅接受列表,并指示该函数将始终返回整数。果然,mypy
发现了问题
mysum.py:10: error:
Argument 1 to "mysum" has incompatible type
"Tuple[int, int, int, int, int]"; expected
↪"List[Any]"
mysum.py:12: error:
Argument 1 to "mysum" has incompatible type
"str"; expected "List[Any]"
好消息是我已经发现了一些问题。但在一种情况下,我正在使用数字元组调用 mysum
,这应该是可以的,但被标记为问题。在另一种情况下,我正在使用整数和字符串的列表调用它,但这被视为很好。
我需要告诉 mypy
,我愿意接受的不仅仅是列表,而是任何序列,例如元组。幸运的是,Python 现在有一个 typing
模块,它为您提供了在这种情况下使用的对象。例如,我可以这样说
from typing import Sequence
def mysum(numbers:Sequence) -> int:
output = 0
for one_number in numbers:
output += one_number
return output
我从 typing
模块中抓取了 Sequence
,它包括所有三种 Python 序列类型——字符串、列表和元组。一旦我这样做,所有的 mypy
问题都消失了,因为所有的参数都是序列。
诚然,这有点过分了。我真正想说的是,我将接受任何元素为整数的序列。我可以通过将函数的注解更改为
from typing import Sequence
def mysum(numbers:Sequence[int]) -> int:
output = 0
for one_number in numbers:
output += one_number
return output
请注意,我已经将注解修改为 Sequence[int]
。在该更改之后,mypy
现在发现了很多问题
mysum.py:13: error: List item 2 has incompatible type "str";
↪expected "int"
mysum.py:13: error: List item 3 has incompatible type "str";
↪expected "int"
mysum.py:14: error: Argument 1 to "mysum" has incompatible type
↪"str"; expected "Sequence[int]"
我称之为巨大的成功。如果现在有人尝试使用错误类型的值来使用我的函数,它会指出他们的问题。
但是等等:我真的只想允许列表和元组吗?集合呢?它也是可迭代的并且可以包含整数?此外,这种对整数的痴迷是什么?我不应该也允许浮点数吗?
我可以通过说我将接受的不是 Sequence[int]
,而是 Iterable[int]
来解决第一个问题——意思是,任何可迭代并返回整数的东西。换句话说,我可以这样说
from typing import Iterable
def mysum(numbers:Iterable[int]) -> int:
output = 0
for one_number in numbers:
output += one_number
return output
最后,我如何允许整数或字符串?我使用特殊的 Union
类型,它允许您将类型组合在方括号中
from typing import Iterable, Union
def mysum(numbers:Iterable[Union[int, float]]) ->
↪Union[int,float]:
output = 0
for one_number in numbers:
output += one_number
return output
但是,如果我对这段代码运行 mypy
,并尝试使用包含至少一个浮点数的可迭代对象调用 mysum
,我将收到一个错误
mysum.py:9: error: Incompatible types in assignment
↪(expression has type "float", variable has type "int")
问题是什么?简而言之,当我将 output
创建为变量时,我给它一个整数值。然后,当我尝试向其中添加浮点值时,我从 mypy
收到警告。因此,我可以注释变量来消除警告
def mysum(numbers:Iterable[Union[int, float]])
↪-> Union[int,float]:
output : Union[int,float] = 0
for one_number in numbers:
output += one_number
return output
果然,该函数现在已经被很好地注解了。我太有经验了,知道这会捕获并解决所有问题,但是如果我的团队中想要使用我的函数的其他人使用 mypy
来检查类型,他们会收到警告。这就是这里的重点,在问题接近生产之前就捕获它们。
您可以在此处阅读有关 mypy
的更多信息。该站点包含文档、教程,甚至为使用 Python 2 并希望通过注释(而不是注解)引入 mypy
的人员提供信息。