Python 3.5中的类型提示是什么?
Python 3.5中的类型提示是什么?
Python3.5中最受关注的功能之一是类型提示。
这篇文章和这篇文章提到了类型提示的示例,并提示要负责地使用类型提示。能否有人解释更多信息并说明何时应该使用它们,何时不应该?
我建议阅读PEP 483和PEP 484,并观看Guido的类型提示演示。
简而言之: 类型提示字面上就是提示你正在使用的对象的类型。
由于Python的动态性,推断或检查正在使用的对象的类型特别困难。这使得开发人员很难理解他们没有编写的代码中究竟发生了什么,更重要的是,对于许多IDE中的类型检查工具(PyCharm和PyDev等),它们由于没有任何指示对象类型的指示器而受到限制。因此,它们尝试使用(正如演示中所提到的)大约50%的成功率来推断类型。
从类型提示演示中取出两张重要的幻灯片:
为什么使用类型提示?
- 帮助类型检查器:通过提示你希望对象具有的类型,类型检查器可以轻松检测出你是否正在传递一个类型不符合预期的对象。
- 有助于文档编写:第三方查看你的代码时会知道在哪里需要什么,因此可以在不获取
TypeErrors
的情况下使用它。 - 有助于IDE开发更精准和稳健的工具:开发环境在了解你的对象类型后,将更适合建议适当的方法。你可能在某个时候使用某个IDE时已经经历过这种情况:点击
.
时会弹出未为对象定义的方法/属性。
为什么使用静态类型检查工具?
- 更早发现错误:这是不言而喻的。
- 项目越大,越需要它:同样是有道理的。静态语言提供了动态语言所缺乏的健壮性和控制力。你的应用程序越大和更复杂,你需要越多的控制和预测性(从行为方面)。
- 大型团队已经在运行静态分析:我猜这验证了前两点。
在这个小介绍的结束语中:这是一个可选功能,并且据我所知,它是为了获得静态类型的一些好处而引入的。
一般情况下,您不需要担心它,并且绝对不需要使用它(特别是在使用Python作为辅助脚本语言的情况下)。当开发大型项目时,它应该很有帮助,因为它提供了非常需要的强大性、控制力和额外的调试功能。
使用mypy进行类型提示:
为了使这个答案更加完整,我认为做一个小示例比较合适。我将使用mypy
库,这个库启发了PEP中介绍的类型提示。这主要是为了那些遇到这个问题并想知道从哪里开始的人。
在我示范之前,让我再强调一下:PEP 484并不强制执行任何事情;它只是为函数注解设置一个方向,并提出了类型检查的指南,以确定执行类型检查的方式。您可以对函数添加注释并提示尽可能多的内容;在Python本身不使用这些注释的情况下,您的脚本仍然可以运行。
不管怎么样,正如PEP中所提到的,提示类型通常应采用三种形式:
- 函数注释(PEP 3107)。
- 内置/用户模块的存根文件。
- 特殊的 type: type 注释,用于补充前两种形式。(参见:Python3.6中 type: type 注释的更新)
此外,您还将想要与在Py3.5中引入的新的typing
模块一起使用类型提示。在其中,定义了许多(附加的)抽象基类(ABC),以及用于静态检查的辅助函数和装饰器。在collections.abc中包括了大多数ABC,但是以通用形式包括,以便允许订阅(通过定义一个__getitem__()方法)。
如果有兴趣深入了解这些内容,可以查看mypy文档
,文档写得很好,还有很多代码示例演示/描述了检查器的功能;绝对值得一读。
函数注释和特殊注释:
首先,有趣的是观察一些使用特殊注释时可以得到的行为。可以在变量分配期间添加特殊# type: type
注释以指示对象的类型,如果不能直接推断出对象类型,这非常有用。一般来说,简单的赋值很容易被推断出来,但其他一些,比如列表(关于它们的内容),可能无法推断。
注意:如果我们想使用任何容器的派生类并需要指定该容器的内容,我们必须使用typing
模块中的通用类型。 它们支持索引。
# Generic List, supports indexing. from typing import List # In this case, the type is easily inferred as type: int. i = 0 # Even though the type can be inferred as of type list # there is no way to know the contents of this list. # By using type: List[str] we indicate we want to use a list of strings. a = [] # type: List[str] # Appending an int to our list # is statically not correct. a.append(i) # Appending a string is fine. a.append("i") print(a) # [0, 'i']
如果我们将这些命令添加到文件中并使用解释器执行它们,一切都可以正常工作,并且print(a)
仅打印列表a
的内容。 # type
注释已被丢弃,被视为没有额外语义含义的纯注释。
另一方面,通过mypy
运行此代码,我们将获得以下响应:
(Python3)jimmi@jim: mypy typeHintsCode.py typesInline.py:14: error: Argument 1 to "append" of "list" has incompatible type "int"; expected "str"
表明str
对象列表不能包含int
,从静态角度来看是正确的。可以通过遵守a
的类型并仅附加str
对象或更改a
的内容的类型来解决此问题,以指示接受任何值(在从typing
导入Any
之后直观地执行List[Any]
)。
函数注释在函数签名的每个参数后面以 param_name : type
的形式添加,并且在结束函数冒号之前使用 -> type
表示法指定返回类型;所有注释以便于字典形式存储在该函数的 __annotations__
属性中。使用一个简单的例子(不需要来自 typing
模块的额外类型):
def annotated(x: int, y: str) -> bool: return x < y
现在 annotated.__annotations__
属性具有以下值:
{'y':, 'return': , 'x': }
如果我们是完全的新手,或者我们熟悉 Python 2.7 的概念,因此不知道在比较 annotated
时潜在的 TypeError
,我们可以进行另一个静态检查,捕获错误并节省我们一些麻烦:
(Python3)jimmi@jim: mypy typeHintsCode.py typeFunction.py: note: In function "annotated": typeFunction.py:2: error: Unsupported operand types for > ("str" and "int")
除此之外,调用带有无效参数的函数也会被捕获:
annotated(20, 20) # mypy complains: typeHintsCode.py:4: error: Argument 2 to "annotated" has incompatible type "int"; expected "str"
这些可以扩展到基本上任何用例,捕捉的错误超出了基本的调用和操作。你可以检查的类型真的很灵活,我只是给出了一小部分其潜力的漏斗。查看 typing
模块、PEP 或 mypy
文档将为您提供更全面的功能。
桩文件:
桩文件可用于两种不相互排斥的情况:
- 您需要类型检查一个模块,但不想直接更改函数签名
- 您希望编写模块并进行类型检查,但还希望将注释与内容分开。
具有扩展名为 .pyi
的 stub 文件是您要创建/使用的模块的带注释接口。它们包含您要进行类型检查的函数的签名,但是函数体被丢弃。为了感受这一点,给定一个名为 randfunc.py
的模块中的三个随机函数集:
def message(s): print(s) def alterContents(myIterable): return [i for i in myIterable if i % 2 == 0] def combine(messageFunc, itFunc): messageFunc("Printing the Iterable") a = alterContents(range(1, 20)) return set(a)
我们可以创建一个名为randfunc.pyi
的存根文件,在其中我们可以设置一些限制,如果希望的话。缺点是没有使用存根的人在尝试理解应该在哪里传递内容时将无法得到注释帮助。
无论如何,存根文件的结构非常简单:添加所有空函数定义(pass
填充)并根据需要提供注释。在这里,假设我们只想使用int
类型来处理容器。
# Stub for randfucn.py from typing import Iterable, List, Set, Callable def message(s: str) -> None: pass def alterContents(myIterable: Iterable[int])-> List[int]: pass def combine( messageFunc: Callable[[str], Any], itFunc: Callable[[Iterable[int]], List[int]] )-> Set[int]: pass
combine
函数说明了为什么你可能想要在不同的文件中使用注释,它们有时会使代码过于冗长,并降低可读性(对Python来说是大忌)。你当然可以使用类型别名,但有时这会更加混乱(因此要明智使用)。
这应该让你熟悉 Python 中类型提示的基本概念。虽然使用的类型检查器是 mypy
,但你应该逐渐开始看到更多的类型提示,一些是 IDE 内部的 (PyCharm), 其他作为标准 Python 模块。
如果我发现(或建议)其他检查器/相关包,我会尝试在下面的列表中添加。
我知道的检查器:
相关的包/项目:
- typeshed: 官方Python存储库,包含标准库的各种存根文件。
typeshed
项目实际上是您可以查看如何在自己的项目中使用类型提示的最佳位置之一。让我们以相应的.pyi
文件中的Counter
类的__init__
dunder为例:
class Counter(Dict[_T, int], Generic[_T]): @overload def __init__(self) -> None: ... @overload def __init__(self, Mapping: Mapping[_T, int]) -> None: ... @overload def __init__(self, iterable: Iterable[_T]) -> None: ...
定义通用类使用_T = TypeVar('_T')
。对于Counter
类,我们可以看到它可以在初始化器中不带参数,从任何类型到int
的单个Mapping
获取或获取任何类型的Iterable
。
注意:我忘了提到的一件事是,typing
模块是在暂定基础上引入的。来自PEP 411:
暂定包的API可能会在“毕业”为“稳定”状态之前进行修改。一方面,这种状态为软件包提供了正式成为Python发行版一部分的优势。另一方面,核心开发团队明确声明不会对软件包的API稳定性作出承诺,下一个版本的API可能会发生变化。虽然这被认为是不太可能的结果,但如果涉及API或维护方面的问题被证明是成立的,则这些软件包甚至可能在没有停用期的情况下从标准库中删除。
因此,对这里的事情保持谨慎; 我怀疑它将被删除或在重大程度上进行修改,但人们永远无法知道。
另一个完全不同的主题,但在类型提示的范围内很有效:PEP 526 : 变量注释语法是一项努力,旨在通过引入新语法来替换#type注释,这允许用户在简单的varname: type语句中注释变量的类型。参见之前提到的What are variable annotations?作为这些内容的简要介绍。