这是关于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%准确的自动完成,请看这个例子:
我的编辑器不能为text
提供任何方法建议,因为它完全不清楚text
的类型可能是什么。
不过,在函数的参数中添加一个简单的: str
类型提示就可以解决这个问题:
有意义的返回类型
同样的论点也适用于函数的返回类型,包括内置的和用户定义的。你将获得100%的自动完成(和更好的理解)关于函数调用实际返回的内容。请看这个例子:
def do_some_stuff(numbers):
d = {x:x**2 for x in numbers}
return d.items()
这个函数的返回类型是什么?让Pylance告诉我们
这个函数do_some_stuff
同时返回一个字典的键和值。如果没有 Pylance 和它的功能,你会得到一些关于变量what_is_this
的类型的信息,但只有一个非常基本的信息,即tuple
的类型。你不会知道这个元组有多少个元素,也不知道它们的类型。相比之下,使用Pylance,你会得到一个更详细的反馈,关于items()
方法的正确返回类型,也就是ItemsView[int, int]
。这确实非常有用!你现在知道,what_is_this
为两个int
元素持有一个ItemView
实例。
如果我们稍微改变一下函数,改成返回平方根,Pylance就会提供给我们一个正确的更新的反馈,即字典的值不再是整数,而是现在的类型float
。
不再需要阅读冗长的文档或在交互式 shell 中尝试一些代码来了解一个函数到底返回了什么,只要让类型提示自己说话就可以了。然而,这就要求我们编码人员首先要有礼貌和纪律地提供正确的类型提示。
有意义的嵌套类型
让我们在这个例子上多呆一会儿。到目前为止,我还没有在函数do_some_stuff
,我只是依靠Pylance的能力来推断出正确的类型。如果我在这个函数中加入类型提示,我应该如何注释numbers
这个参数?因为我在第2行对它进行了迭代,所以它应该是某种Iterable
。如果我用list
来注释它,这就意味着你只能用一个列表来调用这个函数,而不能像例子中使用的range
一样,使用其他的可迭代对象。
typing
模块提供了各种各样的类型,你可以在更复杂的情况下使用适当的类型提示。对于目前的情况,你可以在typing.Iterable
和typing.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])
这段代码在进行类型检查时不会产生任何问题。但是当我试图传递一个带有数字以外的项目的列表时,我将得到一个类型错误。
我从typing
模块中导入了Iterable
的类型提示,并指定其元素为float
的类型。符号OuterType[InnerType]
意味着外部对象是某种容器,内部对象具有某种类型。然而,OuterType
和InnerType
不得不同。所以当你想对一个列表和它的元素进行类型注释时,使用list[<type>]
,例如list[int]
用于整数列表,或者list[str]
用于字符串列表,甚至list[list[int]]
用于嵌套的整数列表 (记住,在 Python 3.9 之前,你必须从typing
导入List
,而不是使用本地list
类型)。如果你想对一个字典进行类型注释,你必须为键和值都提供类型提示,例如:dict[str, list]
,用于一个以字符串为键、以列表为值的字典。让列表中的元素的类型不被定义也是可以的,但要尽量做到具体和严格。