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 来检查所有这些?”
您可以将 x
和 y
都注释为 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 中类型注释的起源以及如何使用它们的信息,可在线访问此处。