Python中类型注释系统的系列文章的第二部分

64 阅读6分钟

这是关于Python中类型注释系统的系列文章的第二部分,简称类型提示。第一部分给出了对类型提示的介绍。

这篇文章是针对类型提示的新手,希望能帮助你入门

在第二部分中,我将讲述一些精心挑选的例子,说明如何使用基本的类型提示来解决一个特定的问题,并提高整个代码的质量和可读性。每个例子都将涵盖某个主题,并从一个稍微不同的角度来看待类型提示。大多数例子的结尾都会有一个提示或最佳实践,以帮助你提高你的类型提示技能!

本系列的下一篇文章将继续介绍类型提示的更高级用途。

第一组例子

第一部分中,你已经了解了静态和动态类型,以及如何和为什么用简单的类型提示来注释一个函数。在这里,我想向你展示几个Python中类型提示的例子,并说一两句为什么它们在每种情况下都可能是有用的。

关于typing 模块 ([4]) 的一个简短的附带说明。你总是可以使用基本类型int,float,str,bool,bytes 而不需要任何导入语句 (见[6]中的小抄!)。这是最基本的类型提示水平。更复杂的数据类型,如list,dict,tuple, 和set ,需要从typing 导入它们自己的类型提示。例如,typing.List 是对list 类型的正确类型提示。然而,从Python 3.9开始,这些数据类型中的一些可以直接作为类型提示,所以list[int] 在Python 3.9中是允许的,但在以前的版本中必须是List[int] 。你可能会看到这两种情况,所以不要感到困惑。当一个东西前面有一个冒号时,你可以确定它是一个类型提示(除了切片和字典定义,但我认为它们很容易区分)。

我选择的编辑器是Visual Studio Code(VSC),我使用Python扩展和可选的Pylance语言服务器,所以你知道我记录截图时的设置。关于如何设置VSC,在本系列的后面部分会有更多介绍。

更好的自动完成

首先,我想提倡类型提示,以支持你最喜欢的编辑器的100%准确的自动完成,请看这个例子:

VSC example no suggestion

我的编辑器不能为text 提供任何方法建议,因为它完全不清楚text 的类型可能是什么。

不过,在函数的参数中添加一个简单的: str 类型提示就可以解决这个问题:

VSC example suggestions

有意义的返回类型

同样的论点也适用于函数的返回类型,包括内置的和用户定义的。你将获得100%的自动完成(和更好的理解)关于函数调用实际返回的内容。请看这个例子:

def do_some_stuff(numbers):
    d = {x:x**2 for x in numbers}
    return d.items()

这个函数的返回类型是什么?让Pylance告诉我们

VSC example better return 1

这个函数do_some_stuff 同时返回一个字典的键和值。如果没有 Pylance 和它的功能,你会得到一些关于变量what_is_this 的类型的信息,但只有一个非常基本的信息,即tuple 的类型。你不会知道这个元组有多少个元素,也不知道它们的类型。相比之下,使用Pylance,你会得到一个更详细的反馈,关于items() 方法的正确返回类型,也就是ItemsView[int, int] 。这确实非常有用!你现在知道,what_is_this 为两个int 元素持有一个ItemView 实例。

如果我们稍微改变一下函数,改成返回平方根,Pylance就会提供给我们一个正确的更新的反馈,即字典的值不再是整数,而是现在的类型float

VSC example better return 2

不再需要阅读冗长的文档或在交互式 shell 中尝试一些代码来了解一个函数到底返回了什么,只要让类型提示自己说话就可以了。然而,这就要求我们编码人员首先要有礼貌和纪律地提供正确的类型提示

有意义的嵌套类型

让我们在这个例子上多呆一会儿。到目前为止,我还没有在函数do_some_stuff ,我只是依靠Pylance的能力来推断出正确的类型。如果我在这个函数中加入类型提示,我应该如何注释numbers 这个参数?因为我在第2行对它进行了迭代,所以它应该是某种Iterable 。如果我用list 来注释它,这就意味着你只能用一个列表来调用这个函数,而不能像例子中使用的range 一样,使用其他的可迭代对象。

typing 模块提供了各种各样的类型,你可以在更复杂的情况下使用适当的类型提示。对于目前的情况,你可以在typing.Iterabletyping.Sequence 之间进行选择,这取决于你是否想强调__iter____getitem__ 被传递给numbers 参数的属性所实现。因为range 函数返回一个同时实现了__iter____getitem__ 的对象(所以你可以用for x in range(10) 遍历它,用range(10)[n] 直接访问它的元素,这一点我直到现在才知道!),两种类型的提示都是合适的。相比之下,zip 函数只为__iter__ 提供了一个实现,所以typing.Iterable 将是合适的,但不是typing.Sequence

在决定使用typing.Iterable 之后,你完成了吗?是的,也不是,取决于你想得到多具体的东西。通过Iterable ,你告知(如果类型检查的话,要求)你期望传递一个可迭代的对象,但不是期望它的元素是什么。例如,字符串也是可迭代对象,但在这种情况下会失败。你必须明确地要求一个迭代对象的元素是数字类型的。如果你没有指定元素(或任何变量)的类型,它们的类型提示默认为Any ,或在Pylance的情况下Unknown ,这是Any 的隐含版本。

因此,让我为numbers 参数的类型提示提出建议:

from typing import Iterable

def do_some_stuff(numbers: Iterable[float]):
    d = {x:x**0.5 for x in numbers}
    return d.items()

do_some_stuff([1,2,3,4.4])

这段代码在进行类型检查时不会产生任何问题。但是当我试图传递一个带有数字以外的项目的列表时,我将得到一个类型错误。

VSC example type error

我从typing 模块中导入了Iterable 的类型提示,并指定其元素为float 的类型。符号OuterType[InnerType] 意味着外部对象是某种容器,内部对象具有某种类型。然而,OuterTypeInnerType 不得不同。所以当你想对一个列表和它的元素进行类型注释时,使用list[<type>] ,例如list[int] 用于整数列表,或者list[str] 用于字符串列表,甚至list[list[int]] 用于嵌套的整数列表 (记住,在 Python 3.9 之前,你必须从typing 导入List ,而不是使用本地list 类型)。如果你想对一个字典进行类型注释,你必须为键和值都提供类型提示,例如:dict[str, list] ,用于一个以字符串为键、以列表为值的字典。让列表中的元素的类型不被定义也是可以的,但要尽量做到具体和严格