介绍 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 的人员提供信息。

Reuven Lerner 在世界各地的公司教授 Python、数据科学和 Git。您可以订阅他的免费每周“更好的开发者”电子邮件列表,并从他的书籍和课程中学习,网址为 http://lerner.co.il。Reuven 与他的妻子和孩子住在以色列的 Modi'in。

加载 Disqus 评论