Python 的 Mypy:可调用对象和生成器

了解 Mypy 的类型检查如何与函数和生成器一起工作。

在我之前的两篇文章中,我描述了 Mypy(Python 的类型检查器)可以帮助识别代码中潜在问题的一些方法。[请参阅“Mypy 简介,Python 的实验性可选静态类型检查器”“Python 的 Mypy—高级用法”。] 对于长期以来喜欢动态语言的人(比如我)来说,Mypy 可能看起来像是倒退了一步。但是,鉴于许多关键任务项目都是用 Python 编写的,而且通常是由沟通有限且 Python 经验不足的大型团队编写的,某种类型的类型检查变得越来越有必要。

重要的是要记住,Python 语言本身并没有改变,也没有变成静态类型语言。 Mypy 是一个独立的程序,在 Python 之外运行,通常作为持续集成 (CI) 系统的一部分或作为 Git 提交钩子的一部分调用。其想法是 Mypy 在您将代码投入生产之前运行,识别数据与您对变量和函数参数所做的注释不匹配的地方。

我将在这里重点介绍 Mypy 的一些高级功能。您可能不会经常遇到它们,但即使您没有遇到,它也会让您更好地了解与类型检查相关的复杂性,以及 Mypy 团队对他们的工作思考的深度,以及需要完成哪些测试。它还将帮助您更多地了解人们进行类型检查的方式,以及如何在动态类型的优美性、灵活性和表现力与静态类型的严格性和更少错误之间取得平衡。

可调用类型

当我在 Python 课程中告诉学员 Python 中的一切都是对象时,他们点头表示同意,显然在想,“我以前在其他语言中听说过这个。” 但是当我向他们展示函数和类都是对象时,他们意识到 Python 的“一切”概念比他们的概念更广泛。(是的,Python 对“一切”的定义不如 Smalltalk 那么广泛。)

当您定义一个函数时,您正在创建一个新对象,类型为“function”


>>> def foo():
...     return "I'm foo!"

>>> type(foo)
<class 'function'>

类似地,当您创建一个新类时,您正在向 Python 添加一个新的对象类型


>>> class Foo():
...     pass

>>> type(Foo)
<class 'type'>

在 Python 中,编写一个函数是很常见的范例,该函数在运行时定义并运行一个内部函数。 这也称为“闭包”,它有几种不同的用途。 例如,您可以编写


def foo(x):
   def bar(y):
       return f"In bar, {x} * {y} = {x*y}"
   return bar

然后您可以运行


b = foo(10)
print(b(2))

您将得到以下输出


In bar, 10 * 2 = 20

我不想赘述这一切是如何运作的,包括内部函数和 Python 的作用域规则。 但是,我想问一个问题“您如何使用 Mypy 来检查所有这些?”

您可以将 xy 都注释为 int。 并且您可以将 bar 的返回值注释为字符串。 但是您如何注释 foo 的返回值呢? 鉴于如上所示,函数是 function 类型,也许您可以使用它。 但是 function 实际上在 Python 中不是一个公认的名称。

相反,您需要使用 typing 模块,该模块随 Python 3 一起提供,因此您可以进行这种类型的类型检查。 并且在 typing 中,名称 Callable 被定义为专门用于此目的。 所以你可以这样写


from typing import Callable


def foo(x: int) -> Callable:
   def bar(y: int) -> str:
       return f"In bar, {x} * {y} = {x*y}"
   return bar


b = foo(10)
print(b(2))

果然,这通过了 Mypy 的检查。 函数 foo 返回 Callable,这是一个包括函数和类的描述。

但是,等一下。 也许您不仅仅想检查 foo 是否返回 Callable。 也许您还想确保它返回一个以 int 作为参数的函数。 为此,您将在单词 Callable 后使用方括号,在这些括号中放入两个元素。 第一个将是参数类型列表(在本例中为单元素列表)。 列表中的第二个元素将描述函数的返回类型。 换句话说,现在的代码看起来像这样


#!/usr/bin/env python3

def foo(x: int) -> Callable[[int], str]:
   def bar(y: int) -> str:
       return f"In bar, {x} * {y} = {x*y}"
   return bar


b = foo(10)
print(b(2))

生成器

在谈论所有这些可调用对象时,您还应该考虑生成器函数会发生什么。 Python 喜欢迭代,并鼓励您尽可能使用 for 循环。 在许多情况下,以函数的形式表达迭代器是最容易的,这在 Python 世界中被称为“生成器函数”。 例如,您可以创建一个生成器函数,该函数返回斐波那契数列,如下所示


def fib():
   first = 0
   second = 1
   while True:
       yield first
       first, second = second, first+second

然后,您可以按如下方式获取前 50 个斐波那契数


g = fib()
for i in range(50):
   print(next(g))

这很棒,但是如果您想将 Mypy 检查添加到您的 fib 函数中怎么办? 似乎您可以简单地说返回值是一个整数


def fib() -> int:
   first = 0
   second = 1
   while True:
       yield first
       first, second = second, first+second

但是,如果您尝试通过 Mypy 运行此代码,您会得到一个非常严厉的响应


atf201906b.py:4: error: The return type of a generator function
should be "Generator" or one of its supertypes
atf201906b.py:14: error: No overload variant of "next" matches
argument type "int"
atf201906b.py:14: note: Possible overload variant:
atf201906b.py:14: note:   def [_T] next(i: Iterator[_T]) -> _T
atf201906b.py:14: note:   <1 more non-matching overload not
shown>

哇! 怎么回事?

好吧,重要的是要记住,运行生成器函数的结果不是您每次迭代产生的结果。 相反,结果是一个生成器对象。 反过来,生成器对象在每次迭代中产生特定的类型。

因此,您真正想做的是告诉 Mypy fib 将返回一个生成器,并且在生成器的每次迭代中,您都会得到一个整数。 您会认为您可以这样做


from typing import Generator

def fib() -> Generator[int]:
   first = 0
   second = 1
   while True:
       yield first
       first, second = second, first+second

但是,如果您尝试运行 Mypy,您会得到以下结果


atf201906b.py:6: error: "Generator" expects 3 type arguments,
but 1 given

事实证明,Generator 类型可以(可选地)在方括号中获取参数。 但是,如果您提供任何参数,则必须提供三个

  • 每次迭代返回的类型——您通常从迭代器中想到的类型。
  • 如果您在生成器上调用 send 方法,生成器将接收的类型。
  • 生成器完全退出时将返回的类型。

由于只有第一个与此程序相关,因此您将为其他每个值传递 None


from typing import Generator

def fib() -> Generator[int, None, None]:
   first = 0
   second = 1
   while True:
       yield first
       first, second = second, first+second

果然,它现在通过了 Mypy 的测试。

结论

您可能会认为 Mypy 无法胜任处理复杂的类型问题,但实际上它已经被考虑得相当周全。 当然,我在这里(以及我在之前关于 Mypy 的两篇文章中)展示的只是开始; Mypy 作者已经解决了各种各样的问题,从模块相互引用彼此的类型到别名长类型描述。

如果您正在考虑加强您组织的代码,那么通过 Mypy 添加类型检查是一个很好的方法。 越来越多的组织正在一点一点地添加其检查,并且正在享受动态语言倡导者长期以来忽略的东西,即如果计算机可以检查您正在使用的类型,您的程序实际上可能会运行得更顺畅。

资源

您可以在此处阅读更多关于 Mypy 的信息。 该站点有文档、教程,甚至为使用 Python 2 并希望通过注释(而不是注释)引入 mypy 的人员提供信息。

您可以在 PEP(Python 增强提案)484 中阅读更多关于 Python 中类型注释的起源以及如何使用它们的信息,可在线访问此处

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

加载 Disqus 评论