类型检查
类型系统
所有编程语言都包含某种类型系统,该系统形式化了它可以处理哪些对象类别以及如何处理这些类别。例如,类型系统可以定义数值类型,以数值类型的对象为例。
动态类型
Python是一种动态类型语言。这意味着Python解释器仅在代码运行时进行类型价差,并且允许变量的类型在其生存期内更改。
示例:
>>> if False:
... 1 + "two" # This line never runs, so no TypeError is raised
... else:
... 1 + 2
...
3
>>> 1 + "two" # Now this is type checked, and a TypeError is raised
TypeError: unsupported operand type(s) for +: 'int' and 'str'
验证变量是否可以更改类型:
>>> thing = "Hello"
>>> type(thing)
<class 'str'>
>>> thing = 28.1
>>> type(thing)
<class 'float'>
这些示例确认允许更改类型,并且Python会在类型更改时正确推断类型
静态类型
动态类型的反面是静态类型。静态类型检查是在不运行程序的情况下执行的。在大多数静态类型语言中,例如 C 和 Java,这是在编译程序时完成的。
使用静态类型时,通常不允许变量更改类型,尽管可能存在将变量转换为其他类型的机制。
让我们看一个来自静态类型语言的快速示例。请考虑以下 Java 代码段:
String_ thing;
thing_=_"Hello";
第一行声明变量名在编译时绑定到类型。该名称永远不能重新绑定到其他类型。在第二行中,分配了一个值。永远不能为其分配不是对象的值。例如,如果你以后说编译器会因为不兼容的类型而引发错误。
Python将始终保持动态类型语言。但是,PEP 484 引入了类型提示,这使得也可以对 Python 代码进行静态类型检查。
与类型在大多数其它静态类型语言中的工作方式不同,类型提示本身不会导致Python强制类型。顾名思义,类型提示只是建议类型。
鸭子类型(Duck Typing)
谈论Python时经常使用的另一个术语是
鸭子类型
。这个绰号来自短语“如果它像鸭子一样走路,像鸭子一样嘎嘎叫,那么它一定是鸭子”(或其任何变体)。
鸭子类型是一个与动态类型相关的概念,其中对象的类型或类不如它定义的方法重要。使用鸭子类型,您根本不检查类型。相反,您需要检查给定方法或属性是否存在。
例如,您可以调用定义方法的任何 Python 对象:len()``.__len__()
>>> class TheHobbit:
... def __len__(self):
... return 95022
...
>>> the_hobbit = TheHobbit()
>>> len(the_hobbit)
95022
请注意,对 的调用给出了方法的返回值。实际上,实现本质上等效于以下内容:len()``.__len__()``len()
def len(obj):
return obj.__len__()
为了调用,唯一真正的约束是它必须定义一个方法。否则,对象的类型可以与 、 或 不同。len(obj)``obj``.__len__()``str``list``dict``TheHobbit
在使用结构子类型对 Python 代码进行静态类型检查时,在一定程度上支持鸭子类型。
Hello Types
在本节中,你将了解如何向函数添加类型提示。以下函数通过添加适当的大写和装饰行将文本字符串转换为标题:
def headline(text, align=True):
if align:
return f"{text.title()}\n{'-' * len(text)}"
else:
return f" {text.title()} ".center(50, "o")
>>> print(headline("python type checking"))
Python Type Checking
--------------------
>>> print(headline("python type checking", align=False))
oooooooooooooo Python Type Checking oooooooooooooo
是时候进行我们的第一个类型提示了!若要向函数添加有关类型的信息,只需批注其参数并返回值,如下所示:
def headline(text: str, align: bool = True) -> str:
...
语法说参数应该是类型 .同样,可选参数应具有默认值 的类型。
在风格方面,PEP8建议如下:
- 对冒号使用常规规则,即冒号前没有空格,冒号后有一个空格;
- 将参数批注与默认值结合时,请在符号周围使用空格;
- 在箭头周围使用空格。
添加这样的类型提示没有运行时效果:它们只是提示,不会自行强制执行。
要捕获此类错误,您可以使用静态类型检查器。也就是说,一种检查代码类型的工具,而无需实际运行传统意义上的代码。不过,进行类型检查的最常见工具是Mypy。
Mypy应用实例:
将代码放在名为test.py
中:
# test.py
def headline(text: str, align: bool = True) -> str:
if align:
return f"{text.title()}\n{'-' * len(text)}"
else:
return f" {text.title()} ".center(50, "o")
print(headline("python type checking"))
print(headline("use mypy", align="center"))
使用Mypy
:
$mypy test.py
headlines.py:10: error: Argument "align" to "headline" has incompatible
type "str"; expected "bool"
根据类型提示,Mypy 能够告诉我们在第 10 行使用了错误的类型。
若要修复代码中的问题,应更改要传入的参数的值。
优点和缺点
优点
- 类型提示有助于捕获某些错误;
- 类型提示有助于记录代码。传统上,如果要记录函数参数的预期类型,则可以使用文档字符串。这是有效的,但由于文档字符串没有标准(尽管 PEP 257,它们不能轻易用于自动检查。
- 类型提示可帮助您构建和维护更简洁的体系结构。编写类型提示的行为迫使您考虑程序中的类型。虽然 Python 的动态特性是其重要资产之一,但有意识地依赖鸭子类型、重载方法或多种返回类型是一件好事。
缺点
<静态类型检查>
- 添加类型提示需要开发人员花费时间和精力。尽管花更少的时间调试可能会得到回报,但您将花费更多的时间输入代码。
- 类型提示会在启动时间中引入轻微的损失。如果需要使用
键入
模块,导入时间可能会很长,尤其是在短脚本中。
小结
那么,你应该在自己的代码中使用静态类型检查吗?好吧,这不是一个全有或全无的问题。幸运的是,Python支持渐进类型的概念。这意味着您可以逐步将类型引入代码中。静态类型检查器将忽略没有类型提示的代码。因此,您可以开始向关键组件添加类型,只要它为您增加价值,就可以继续添加类型。
查看上面的优缺点列表,您会注意到添加类型对正在运行的程序或程序的用户没有影响。类型检查旨在让您作为开发人员的生活更好、更方便。
关于是否向项目添加类型的一些经验法则是:
- 如果你刚刚开始学习Python,你可以放心地等地类型提示,直到你有更多地经验。
- 类型提示在简短的一次性脚本中几乎没有增加价值。
- 在其它人使用地库中,尤其是在PyPi上发布地,类型提示增加了更多价值。使用库地其他需要这些类型提示才能正确地进行类型检查。
- 在较大的项目中,类型提示可帮助您了解类型在代码中的流动方式,强烈建议使用。
Bernát Gábor在他的优秀文章《Python 中的类型提示状态》中建议“每当单元测试值得编写时,都应该使用类型提示。事实上,类型提示在代码中扮演着与测试类似的角色:它们可以帮助您作为开发人员编写更好的代码。
希望您现在对类型检查在 Python 中的工作方式以及您是否想在自己的项目中使用它有所了解。
Annotations
函数注释
对于函数,您可以批注参数和返回值。这是按如下方式完成的:
def func(arg: arg_type, optarg: arg_type = default) -> return_type:
...
对于参数,语法为 ,而返回类型使用 进行批注。请注意,注释必须是有效的 Python 表达式。
argument: annotation``-> annotation
示例:
import math
def circumference(radius: float) -> float:
return 2 * math.pi * radius
运行代码时,还可以检查批注。它们存储在函数的特殊属性中:.__annotations__
>>> circumference(1.23)
7.728317927830891
>>> circumference.__annotations__
{'radius': <class 'float'>, 'return': <class 'float'>}
变量注释
在上一节的定义中,您只对参数和返回值进行了批注。您没有在函数体内添加任何注释。通常情况下,这就足够了。circumference()
但是,有时类型检查器也需要帮助来确定变量的类型。变量注释在 PEP 526 中定义,并在 Python 3.6 中引入。语法与函数参数注释相同:
pi: float = 3.142
def circumference(radius: float) -> float:
return 2 * pi * radius
变量已使用类型提示进行批注:pi:float
注:静态类型检查器能够确定这是一个浮点数,因此在此示例中不需要注释
您可以在不给变量赋值的情况下对其进行注释。这会将注释添加到字典中,而变量保持未定义状态:__annotations__
>>> nothing: str
>>> nothing
NameError: name 'nothing' is not defined
>>> __annotations__
{'nothing': <class 'str'>}
由于没有赋值,因此尚未给定义名称
Sequences and Mapping(序列和字典)
name:str="Gudio"
pi:float=3.143
centered:bool = False
#复合类型
names:list=["Guido","Jukka","IVan"]
version:tuple=(3,7,1)
options:dict={"centered":False,"capitalize":True}
然而,这并不能真正说明全部情况.提示的信息不全面
from typing import Dict,List,Tuple
names:list[str]=["Guido","Jukka","IVan"]
version:Tuple[int,int,int]=(3,7,1)
options:Dict[str,bool]={"centered":False,"capitalize":True}
请注意,这些类型中的每一个都以大写字母开头,并且它们都使用方括号来定义项目类型:
该模块还包括其它类型:Counter,Deque,FrozenSet,NamedTuple Set
#卡牌函数示意
def create_deck(shuffle: bool = False) -> List[Tuple[str, str]]:
"""Create a new deck of 52 cards"""
deck = [(s, r) for r in RANKS for s in SUITS]
if shuffle:
random.shuffle(deck)
return deck
元组和列表注释方式的不同
元组是不可变的序列,通常由固定数量的可能不同类型的元素组成。例如,我们将一张牌表示为花色和等级的元组。通常,您为 n 元组编写。
Tuple[t_1, t_2, ..., t_n]
列表是一个可变序列,通常由未知数量的相同类型的元素组成,例如卡片列表。无论列表中有多少个元素,注释中都只有一种类型:。
List[t]
在许多情况下,您的函数会期望某种序列,并不真正关心它是列表还是元组。在这些情况下,您应该在注释函数参数时使用:
typing.Sequence
from typing import List, Sequence
def square(elems: Sequence[float]) -> List[float]:
return [x**2 for x in elems]
类型别名
示例
def deal_hands(
deck: List[Tuple[str, str]]
) -> Tuple[
List[Tuple[str, str]],
List[Tuple[str, str]],
List[Tuple[str, str]],
List[Tuple[str, str]],
]:
"""Deal the cards in the deck into four hands"""
return (deck[0::4], deck[1::4], deck[2::4], deck[3::4])
这太可怕了!
回想一下,类型注释是常规 Python 表达式。这意味着您可以通过将类型别名分配给新变量来定义自己的类型别名。例如,您可以创建和键入别名:Card``Deck
from typing import List, Tuple
Card = Tuple[str, str]
Deck = List[Card]
使用这些别名,注释变得更具可读性:deal_hands()
def deal_hands(deck: Deck) -> Tuple[Deck, Deck, Deck, Deck]:
"""Deal the cards in the deck into four hands"""
return (deck[0::4], deck[1::4], deck[2::4], deck[3::4])
类型别名非常适合使代码及其意图更清晰。同时,可以检查这些别名以查看它们代表的内容:
>>> from typing import List, Tuple
>>> Card = Tuple[str, str]
>>> Deck = List[Card]
>>> Deck
typing.List[typing.Tuple[str, str]]
#print:它显示的是2元字符串列表的别名
没有返回值的函数
您可能知道没有显式返回的函数仍然返回 None
:
>>> def play(player_name):
... print(f"{player_name} plays")
...
>>> ret_val = play("Jacob")
Jacob plays
>>> print(ret_val)
None
虽然这些函数在技术上返回一些东西,但该返回值是没有用的。您应该添加类型提示,说明与返回类型一样多:None
# play.py
def play(player_name: str) -> None:
print(f"{player_name} plays")
ret_val = play("Filip")
注释有助于捕获您尝试使用无意义的返回值的细微错误。Mypy
会给你一个有用的警告:
$ mypy play.py
play.py:6: error: "play" does not return a value
请注意,显式表示函数不返回任何内容与不添加有关返回值的类型提示不同:
# play.py
def play(player_name: str):
print(f"{player_name} plays")
ret_val = play("Henrik")
在后一种情况下,Mypy 没有关于返回值的信息,因此它不会生成任何警告:
$ mypy play.py
Success: no issues found in 1 source file
作为一个更奇特的情况,请注意,您还可以注释永远不会正常返回的函数。这是使用 NoReturn
完成的:
from typing import NoReturn
def black_hole() -> NoReturn:
raise Exception("There is no going back ...")
由于总是引发异常,因此它永远不会正确返回。
类型Any
import random
from typing import Any, Sequence
def choose(items: Sequence[Any]) -> Any:
return random.choice(items)
这或多或少意味着它所说的:是一个可以包含任何类型的项的序列,并将返回任何类型的一个此类项。不幸的是,这并没有那么有用。
类型论
子类型(Subtypes)
从形式上讲,如果满足以下两个条件,我们说该类型是TU的子类型;
- 来自的每个值也在TU类型的值集中
- 类型中每个函数也都在UT类型的函数集中
这两个条件保证即使类型与不同,类型的变量也可以始终假装为TUTU。
子类型的重要性在于子类型始终可以伪装成其超类型。
示例:
def double(number: int) -> int:
return number * 2
print(double(True)) # Passing in bool instead of int
Covariant, Contravariant, and Invariant
Tuple
is covariant. This means that it preserves the type hierarchy of its item types: is a subtype of because is a subtype of .Tuple[bool]``Tuple[int]``bool``int
List
is invariant. Invariant types give no guarantee about subtypes. While all values of are values of , you can append an to and not to . In other words, the second condition for subtypes does not hold, and is not a subtype of .List[bool]``List[int]``int``List[int]``List[bool]``List[bool]``List[int]
Callable
is contravariant in its arguments. This means that it reverses the type hierarchy. You will see how works later, but for now think of as a function with its only argument being of type . An example of a is the function defined above. Being contravariant means that if a function operating on a is expected, then a function operating on an would be acceptable.Callable``Callable[[T], ...]``T``Callable[[int], ...]``double()``bool``int
使用Python类型
类型变量
类型变量是一种特殊变量,可以根据情况采用任何类型。
让我们创建一个类型变量,它将有效地封装以下行为:choose()
# choose.py
import random
from typing import Sequence, TypeVar
Choosable = TypeVar("Choosable")
def choose(items: Sequence[Choosable]) -> Choosable:
return random.choice(items)
names = ["Guido", "Jukka", "Ivan"]
reveal_type(names)
name = choose(names)
reveal_type(name)
必须从模块中使用定义类型变量。使用时,类型变量的范围涵盖所有可能的类型,并采用最具体的类型。在示例中,现在是一个:TypeVar``typing``name``str
$ mypy choose.py
choose.py:12: error: Revealed type is 'builtins.list[builtins.str*]'
choose.py:15: error: Revealed type is 'builtins.str*'