全⾯理解Python中的类型提⽰(TypeHints)
众所周知,Python 是动态类型语⾔,运⾏时不需要指定变量类型。这⼀点是不会改变的,但是2015年9⽉创始⼈ Guido van Rossum 在Python 3.5 引⼊了⼀个类型系统,允许开发者指定变量类型。它的主要作⽤是⽅便开发,供IDE 和各种开发⼯具使⽤,对代码运⾏不产⽣影响,运⾏时会过滤类型信息。
Python的主要卖点之⼀就是它是动态类型的,这⼀点也不会改变。⽽在2014年9⽉, (Python ) 创建了⼀个Python增强提议(),为Python添加类型提⽰(Type Hints)。并在⼀年后,于2015年9⽉作为Python3.5.0的⼀部分发布了。于是对于的Python,有了⼀种标准⽅法向代码中添加类型信息。在这篇博⽂中,我将探讨这个系统是如何成熟的,我们如何使⽤它以及类型提⽰的下⼀步是什么。
为什么需要类型提⽰?
类型提⽰的优点
⾸先,让我们看看为什么需要Python中的类型提⽰(Type Hints)。类型提⽰有很多优点,我将尝试按重要性顺序来列举这些优点:
易于理解代码
了解参数的类型,可以使得理解和维护代码库变得更加容易。
例如,现在有⼀个函数。虽然我们在创建函数时知道了参数类型,但⼏个⽉之后就不再是这种情况了。在代码旁边陈述了所有参数的类型和返回类型,可以⼤⼤加快了理解这个代码⽚段的过程。永远记住你阅读的代码,⽐你写的代码要多得多。因此,你应该优化函数以便于阅读。
有了类型提⽰(Type Hints),在调⽤函数时就可以告诉你需要传递哪些参数类型;以及需要扩展/修改函数时,也会告诉你输⼊和输出所需要的数据类型。例如,想象⼀下以下这个发送请求的函数,
1.
def send_request(request_data : Any,
2.
headers: Optional[Dict[str, str]],
3.
user_id: Optional[UserId] = None,
4.
as_json: bool = True):
5.
...
只看这个函数签名,我们就可以知道:
request_data可以是任何数据
header的内容是⼀个可选的字符串字典
UserId是可选的(默认为None),或者是符合编码UserId的任何数据
as_json需要始终是⼀个布尔值(本质上是⼀个flag,即使名称可能没有提供这种提⽰)
事实上,我们中的许多⼈已经明⽩在代码中提供类型信息是必不可少的。但是由于缺乏更好的选择,往往是在⽂档中提到代码中的类型信息。
⽽类型提⽰系统可以将类型信息从⽂档中移动到更加接近函数的接⼝,然后以⼀个明确定义的⽅式来声明复杂的类型要求。同时,构建linters,并在每次更改代码后运⾏它们,可以检查这些类型提⽰约束,确保它们永远不会过时。
lint指代码静态分析⼯具
易于重构
类型提⽰可以在重构时,更好得帮助我们定位类的位置。
虽然许多IDE现在采⽤⼀些启发式⽅法提供了这项功能,但是类型提⽰可以使IDE具有100%的检测准确率,并定位到类的位置。这样可以更平滑,更准确地检测变量类型在代码中的运⾏⽅式。
请记住,虽然动态类型意味着任何变量都可以成为任何类型,但是所有变量在所有时间中都应只有⼀种类型。类型系统仍然是编程的核⼼组件,想想那些使⽤isinstance判断变量类型、应⽤逻辑所浪费的时间吧。
易于使⽤库
使⽤类型提⽰意味着IDE可以拥有更准确、更智能的建议引擎。当调⽤⾃动完成时,IDE会完全放⼼地知
道对象上有哪些⽅法/属性可⽤。此外,如果⽤户尝试调⽤不存在的内容或传递不正确类型的参数,IDE可以⽴即警告它。
Type Linters
尽管IDE警告不正确的参数类型的功能很好,但使⽤linter⼯具扩展这个功能,以确保应⽤程序的逻辑类型则更加明智。这样的⼯具可以帮助你尽早捕获错误(例如,在输⼊后的⽰例必须是str类型,传⼊None会引发异常):
1.
def transform(arg):
2.
return 'transformed value {}'.format(arg.upper())
3.
4.
transform(None) # if arg would be type hinted as str the type linter could warn that this is an invalid call
虽然在这个例⼦中,有些⼈可能会认为很容易看到参数类型不匹配,但在更复杂的情况中,这种不匹配越来越难以看到。例如嵌套函数调⽤:
1.
def construct(param=None):
2.
return None if param is None else ''
3.
4.
def append(arg):
5.
return arg + ' appended'
6.
7.
transform( append( construct() ) )
有许多的linters,但Python类型检查的参考实现是。 mypy是⼀个Python命令⾏应⽤ (Python command line application ),可以轻松集成到我们的代码流中。
验证运⾏数据
类型提⽰可⽤于在运⾏时进⾏验证,以确保调⽤者不会破坏⽅法的约定。不再需要在函数的开始,使⽤⼀长串类型断⾔(type asserts); 取⽽代之,我们可以使⽤⼀个重⽤类型提⽰的框架,并在业务逻辑运⾏之前⾃动检查它们是否满⾜(例如使⽤):
1.
from datetime import datetime
2.
from typing import List
3.
from pydantic import BaseModel, ValidationError
4.
5.
class User(BaseModel):
6.
id: int
7.
name = 'John Doe'
8.
signup_ts: datetime = None
9.
friends: List[int] = []
10.
11.
external_data = {'id': '123', 'signup_ts': '2017-06-01 12:22',
12.
'friends': [1, 2, 3]}
13.
user = User(**external_data)
14.
15.
try:
16.
User(signup_ts='broken', friends=[1, 2, 'not number'])
17.
except ValidationError as e:
18.
print(e.json())
类型检测不能做什么?
从⼀开始,Guido 明确表⽰类型提⽰并不意味着⽤于以下⽤例(当然,这并不意味着没有这样的 library/tools):
没有运⾏时类型推断
运⾏时解释器(CPython)不会尝试在运⾏时推断类型信息,或者验证基于此传递的参数。
没有性能调整
运⾏时解释器(CPython)不使⽤类型信息来优化⽣成的字节码以获得安全性或性能。
执⾏Python脚本时,类型提⽰被视为注释,解释器⾃动忽略。
需要什么样类型系统?
Python具有渐进的类型提⽰;意味着⽆论何时,对于给定的函数或变量,都没有指定类型提⽰。
我们可以假设它可以具有任何类型(即它仍然是动态类型的⼀部分)。
并且逐渐使你的代码库感知类型,例如⼀次⼀个函数或变量:
function arguments,
function return values,
variables.
请记住,只有具有类型提⽰的代码才会类型检查!
当你在具有类型提⽰的代码上运⾏linter(例如 mypy)时,如果存在类型不匹配,则会出现错误:
1.
# tests/test_magic_field.py
2.
f = MagicField(name=1, MagicType.DEFAULT)
3.
f.names()
此代码将⽣成以下输出:
1.
bernat@uvm ~/python-magic (master●)$ mypy --ignore-missing-imports tests/test_magic_field.py
2.
tests/test_magic_field.py:21: error: Argument 1 to "MagicField" has incompatible type "int";
3.
expected "Union[str, bytes]"
4.
tests/test_magic_field.py:22: error: "MagicField" has no attribute "names"; maybe "name" or "_name"?
注意,我们可以检测传⼊的参数的类型不兼容性,以及访问对象上不存在的属性。后者甚⾄可以提供有效的选项,使得更加容易的注意到和修复拼写错误。
如何加⼊类型信息
⼀旦你决定添加类型提⽰,就会发现可以通过很多种⽅式将其添加到代码库中。
Type annotations
1.
from typing import List
2.
3.
class A(object):
4.
def __init__() -> None:
5.
self.elements : List[int] = []
6.
7.
def add(element: int) -> None:
8.
self.elements.append(element)
类型标注(Type annotations)是⼀种直接的⽅式,并且是类型⽂档中最常见到的那种⽅式。
它使⽤通过(Python 3.0+)添加的函数标注以及通过(Python 3.6+)添加的变量标注。这些可以使得在编写代码时,
使⽤:语句将信息附加到变量或函数参数中。,
->运算符⽤于将信息附加到函数/⽅法的返回值中。
这种⽅法的好处是:
这是实现类型提⽰的规范⽅式,这意味着是类型提⽰中最⼲净的⼀种⽅式。
因为类型信息附加在代码的右侧,这样我们可以⽴刻明晰类型。
缺点是:
它不向后兼容。⾄少需要Python 3.6才能使⽤它。
强制你导⼊所有类型依赖项,即使它们根本不在运⾏时使⽤。
在类型提⽰中,会使⽤到复合类型,例如List[int]。⽽为了构造这些复杂类型,解释器在⾸次加载此⽂件时需要执⾏⼀些操作。
这样发现,最后两点与我们之前列出的类型系统的初始⽬标相⽭盾:
即在运⾏时基本上将所有类型信息作为注释处理。
为了解决这些⽭盾,Python 3.7引⼊了。
加⼊以下语句,解释器将不再构造这些复合类型。
from __future__ import annotations
⼀旦解释器解析脚本语法树后,它会识别类型提⽰并绕过评估它,将其保留为原始字符串。这种机制使得类型提⽰发⽣在需要它们的地⽅:由linter来进⾏类型检查。在Python 4中,这种机制将成为默认⾏为。
Type comments
当标注语句(Type annotations)不可⽤时,可以使⽤类型注释:
沿着这条路⾛下去,我们确实得到了⼀些好处:
类型注释适⽤于任何Python版本。
尽管Python 3.5+中才将类型库添加到标准库中,但它可以作为Python 2.7+的PyPi包使⽤。此外,由于Python注释是任何Python代码下的有效语⾔特性,因此可以在Python 2.7或更⾼版本上代码中加⼊类型提⽰。有⼀些要求:类型提⽰注释(type hint comment)必须位于函数/变量定义所在的相同或下⼀⾏。它也以type:constant 开始。
此解决⽅案还解决了包装问题,因为注释很少会被删除。在源代码中打包类型提⽰信息可以使得那些使⽤你开发的库的⼈,使⽤类型提⽰信息来改善他们的开发体验。
但也会产⽣⼀些新问题:
缺点在于,虽然类型信息接近于参数,但是并不在更靠近参数的右边。这使得代码⽐其他⽅式更混乱。还有必须在⼀⾏中,这也可能会导致问题。例如,如果有⼀个长类型提⽰表达式(long type hint expression)并且你的代码库强制执⾏每⼀⾏的长度限制。
另⼀个问题在于,类型提⽰信息会与使⽤这些类型的注释标记的其他⼯具产⽣竞争(例如,抑制其他linter的错误)。
除了强制您导⼊所有类型信息外,这也会导致处于⼀个更加危险的地⽅。现在导⼊的类型仅在代码中使⽤,这使得⼤多数linter⼯具都
认为所有这些导⼊都未使⽤。如果你允许他们删除这些语句,将会破坏代码的type linter。pylint通过将其AST解析器移动到来解决这个问题,并且在Python 3.7第⼆个版本中发布。
为了避免将长⾏代码作为类型提⽰,可以通过类型注释逐个加⼊提⽰参数,然后将代码放⼊在返回类型注释后:
1.
def add(element # type: List[int]
2.
):
3.
# type: (...) -> None
4.
self.elements.append(element)
让我们简单看⼀下类型注释是如何使代码变得更加混乱。
下⾯是⼀个代码⽚段,它在类中交换两个属性值:
1.
from typing import List
2.
3.
class A(object):
4.
def __init__():
5.
# type: () -> None
6.
self.elements = [] # type: List[int]
7.
8.
def add(element):
9.
# type: (List[int]) -> None
10.
self.elements.append(element)
1.
@contextmanager
2.
def swap_in_state(state, config, overrides):
3.
old_config, old_overrides = fig, state.overrides
4.
5.
yield old_config, old_overrides
6.
⾸先,您必须添加类型提⽰。
因为类型提⽰会很长,所以你可以通过参数附加类型提⽰参数:
1.
2.
@contextmanager
3.
def swap_in_state(state, # type: State
4.union是什么类型
config, # type: HasGetSetMutable
5.
overrides # type: Optional[HasGetSetMutable]
6.
):
7.
# type: (...) -> Generator[Tuple[HasGetSetMutable, Optional[HasGetSetMutable]], None, None]

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。