原文地址:https://realpython.com/python-type-checking/
在本指南中,你将了解Python类型检查。传统上,Python解释器以灵活但隐式的方式处理类型。Python的最新版本允许你指定可由不同工具使用的显式类型提示,以帮助您更有效地开发代码。
通过本教程,你将学到以下内容:
类型注解和提示(Type annotations and type hints)
代码里添加静态类型
静态类型检查
运行时强制类型一致
这是一个全面的指南,将涵盖很多领域。如果您只是想快速了解一下类型提示在Python中是如何工作的,并查看类型检查是否包括在您的代码中,那么您不需要阅读全部内容。Hello Types和正反两部分将让您大致了解类型检查是如何工作的,并介绍它在什么时候有用。
Type Systems
所有的编程语言都包括某种类型的系统,该系统将它可以处理的对象类别以及如何处理这些类别形式化。例如,类型系统可以定义一个数字类型,其中42是数字类型对象的一个例子。
动态类型
Python是一种动态类型语言。这意味着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'
在上面例子中,if从未运行过,因此它未被类型检查过。成功运行了else部分得到结果3,紧接着下面计算1 +“2”时,因为类型不一致所以,产生一个类型错误。
看下一个例子,如果改变一个变量的值的类型
thing = "Hello"
type(thing)
thing = 28.1
type(thing)
type()返回对象的类型。这些示例确认允许更改事物的类型,并且Python在更改时正确地推断出类型。
静态类型
与动态类型相反的是静态类型。在不运行程序的情况下执行静态类型检查。在大多数静态类型语言中,编译是在程序时完成的。例如C和Java,
对于静态类型,通常不允许变量改变类型,尽管可能存在将变量转换为不同类型的机制。
让我们看一个静态类型语言的快速示例。请考虑以下Java代码段:
String thing;
thing = "Hello";
第一行声明thing的类型是String,所以后面的赋值也必须指定字符串类型,如果你给thing=2就会出错,但是python就不会出错。
虽然,Python始终是一种动态类型语言。但是,PEP 484引入了类型提示,这使得还可以对Python代码进行静态类型检查。
与大多数其他静态类型语言中的工作方式不同,类型提示本身不会导致Python强制执行类型。顾名思义,键入提示只是建议类型。
鸭子类型
在谈论Python时经常使用的另一个术语是鸭子打字。这个绰号来自短语“如果它像鸭子一样行走,它像鸭子一样嘎嘎叫,那它一定是鸭子”(或其任何变化)。
鸭子类型是一个与动态类型相关的概念,其中对象的类型或类不如它定义的方法重要。使用鸭子类型根本不需要检查类型,而是检查给定方法或属性是否存在。
下面一个例子, 你可在python所有的对象中使用 len() 的魔法函数len() 方法:
class TheHobbit:
... def len(self):
... return 95022
...
the_hobbit = TheHobbit()
len(the_hobbit)
95022
实际len()方法就是下面的这种方法实现的:
def len(obj):
return obj.__len__()
由此发现,对象也可以像str、list
、dict那样使用len方法,只不过需要重新写len魔法函数即可。
Hello Types
在本节中,您将看到如何向函数添加类型提示。下面的函数通过添加适当的大写字母和装饰线将文本字符串转换为标题:
def headline(text, align=True):
if align:
return f"{text.title()}\n{'-' * len(text)}"
else:
return f" {text.title()} ".center(50, "o")
默认情况下,函数返回与下划线对齐的左侧标题。通过将align标志设置为False,您还可以选择使用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:
...
text: str 意思是text值类型是str, 类似的, 可选参数 align 指定其类型为bool并给定默认值True. 最后, -> str 表示函数headline() 返回值类型为str。
在代码风格方面,PEP 8建议如下::
对冒号使用常规规则,即冒号前没有空格,冒号后面有一个空格:text: str。
将参数注释与默认值组合时,在=符号周围使用空格:align: bool = True。
def headline(...) - > str,使用空格围绕。
print(headline("python type checking", align="left"))
Python Type Checking
但是如果传入的参数类型不是指定的参数类型,程序不会出现错误,此时可以使用类型检查模块通过提示内容确定是否类型输入正确,如mypy。
你可以通过 pip安装:
$ pip install mypy
将以下代码放在名为headlines.py的文件中:
headlines.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 headlines.py
headlines.py:10: error: Argument "align" to "headline" has incompatible
type "str"; expected "bool"
根据类型提示,Mypy能够告诉我们我们在第10行使用了错误的类型
这样说明一个问题参数名align不是很好确定参数是bool类型,我们将代码改成下面这样,换一个识别度高的参数名centered。
headlines.py
def headline(text: str, centered: bool = False):
if not centered:
return f"{text.title()}\n{'-' * len(text)}"
else:
return f" {text.title()} ".center(50, "o")
print(headline("python type checking"))
print(headline("use mypy", centered=True))
再次运行文件发现没有错误提示,ok。
$ mypy headlines.py
$
然后就可以打印结果了
$ python headlines.py
Python Type Checking
oooooooooooooooooooo Use Mypy oooooooooooooooooooo
第一个标题与左侧对齐,而第二个标题居中。
Pros and Cons
类型提示的增加方便了IDE的代码提示功能,我们看到下面text使用.即可得到str使用的一些方法和熟悉。
类型提示可帮助您构建和维护更清晰的体系结构。编写类型提示的行为迫使您考虑程序中的类型。虽然Python的动态特性是其重要资产之一,但是有意识地依赖于鸭子类型,重载方法或多种返回类型是一件好事。
需要注意的是,类型提示会在启动时带来轻微的损失。如果您需要使用类型模块,那么导入时间可能很长,尤其是在简短的脚本中。
那么,您应该在自己的代码中使用静态类型检查吗?这不是一个全有或全无的问题。幸运的是,Python支持渐进式输入的概念。这意味着您可以逐渐在代码中引入类型。没有类型提示的代码将被静态类型检查器忽略。因此,您可以开始向关键组件添加类型,只要它能为您增加价值,就可以继续。
关于是否向项目添加类型的一些经验法则:
如果您刚开始学习Python,可以安全地等待类型提示,直到您有更多经验。
类型提示在短暂抛出脚本中增加的价值很小。
在其他人使用的库中,尤其是在PyPI上发布的库中,类型提示会增加很多价值。使用库的其他代码需要这些类型提示才能正确地进行类型检查。
在较大的项目中,类型提示可以帮助您理解类型是如何在代码中流动的,强烈建议您这样做。在与他人合作的项目中更是如此。
Bernat Gabor在他的文章《Python中类型提示的状态》中建议,只要值得编写单元测试,就应该使用类型提示。实际上,类型提示在代码中扮演着类似于测试的角色:它们帮助开发人员编写更好的代码。
Annotations[注解]
Python 3.0中引入了注释,最初没有任何特定用途。它们只是将任意表达式与函数参数和返回值相关联的一种方法。
多年以后,PEP 484根据Jukka Lehtosalo博士项目Mypy所做的工作,定义了如何向Python代码添加类型提示。添加类型提示的主要方法是使用注释。随着类型检查变得越来越普遍,这也意味着注释应该主要保留给类型提示。
接下来的章节将解释注释如何在类型提示的上下文中工作。
Function Annotations[函数注解]
之前我们也提到过函数的注解例子向下面这样:
def func(arg: arg_type, optarg: arg_type = default) -> return_type:
...
对于参数,语法是参数:注释,而返回类型使用->注释进行注释。请注意,注释必须是有效的Python表达式。
以下简单示例向计算圆周长的函数添加注释:
import math
def circumference(radius: float) -> float:
return 2 * math.pi * radius
通调用circumference对象的annotations魔法函数可以输出函数的注解信息。
circumference(1.23)
7.728317927830891
circumference.annotations
{'radius':
有时您可能会对Mypy如何解释您的类型提示感到困惑。对于这些情况,有一些特殊的Mypy表达式:reveal type()和reveal local()。您可以在运行Mypy之前将这些添加到您的代码中,Mypy将报告它所推断的类型。例如,将以下代码保存为reveal.py。
reveal.py
import math
reveal_type(math.pi)
radius = 1
circumference = 2 math.pi radius
reveal_locals()
然后通过mypy运行上面代码
$ mypy reveal.py
reveal.py:4: error: Revealed type is 'builtins.float'
reveal.py:8: error: Revealed local types are:
reveal.py:8: error: circumference: builtins.float
reveal.py:8: error: radius: builtins.int
即使没有任何注释,Mypy也正确地推断了内置数学的类型。以及我们的局部变量半径和周长。
注意:以上代码需要通过mypy运行,如果用python运行会报错,另外mypy 版本不低于 0.610
Variable Annotations[变量注解]
有时类型检查器也需要帮助来确定变量的类型。变量注释在PEP 526中定义,并在Python 3.6中引入。语法与函数参数注释相同:
pi: float = 3.142
def circumference(radius: float) -> float:
return 2 * pi * radius
pi被声明为float类型。
注意: 静态类型检查器能够很好地确定3.142是一个浮点数,因此在本例中不需要pi的注释。随着您对Python类型系统的了解越来越多,您将看到更多有关变量注释的示例。.
变量注释存储在模块级annotations字典中::
circumference(1)
6.284
annotations
{'pi':
即使只是定义变量没有给赋值,也可以通过annotations获取其类型。虽然在python中没有赋值的变量直接输出是错误的。
nothing: str
nothing
NameError: name 'nothing' is not defined
annotations
{'nothing':
Type Comments[类型注解]
如上所述,注释是在Python 3中引入的,并且它们没有被反向移植到Python 2.这意味着如果您正在编写需要支持旧版Python的代码,则无法使用注释。
要向函数添加类型注释,您可以执行以下操作:
import math
def circumference(radius):
# type: (float) -> float
return 2 * math.pi * radius
类型注释只是注释,所以它们可以用在任何版本的Python中。
类型注释由类型检查器直接处理,所以不存在annotations字典对象中:
circumference.annotations
{}
类型注释必须以type: 字面量开头,并与函数定义位于同一行或下一行。如果您想用几个参数来注释一个函数,您可以用逗号分隔每个类型:
def headline(text, width=80, fill_char="-"):
# type: (str, int, str) -> str
return f" {text.title()} ".center(width, fill_char)
print(headline("type comments work", width=40))
您还可以使用自己的注释在单独的行上编写每个参数:
headlines.py
def headline(
text, # type: str
width=80, # type: int
fill_char="-", # type: str
): # type: (...) -> str
return f" {text.title()} ".center(width, fill_char)
print(headline("type comments work", width=40))
通过Python和Mypy运行示例:
$ python headlines.py
---------- Type Comments Work ----------
$ mypy headline.py
$
如果传入一个字符串width="full",再次运行mypy会出现一下错误。
$ mypy headline.py
headline.py:10: error: Argument "width" to "headline" has incompatible
type "str"; expected "int"
您还可以向变量添加类型注释。这与您向参数添加类型注释的方式类似:
pi = 3.142 # type: float
上面的例子可以检测出pi是float类型。
So, Type Annotations or Type Comments?
所以向自己的代码添加类型提示时,应该使用注释还是类型注释?简而言之:尽可能使用注释,必要时使用类型注释。
注释提供了更清晰的语法,使类型信息更接近您的代码。它们也是官方推荐的写入类型提示的方式,并将在未来进一步开发和适当维护。
类型注释更详细,可能与代码中的其他类型注释冲突,如linter指令。但是,它们可以用在不支持注释的代码库中。
还有一个隐藏选项3:存根文件。稍后,当我们讨论向第三方库添加类型时,您将了解这些。
存根文件可以在任何版本的Python中使用,代价是必须维护第二组文件。通常,如果无法更改原始源代码,则只需使用存根文件。
玩转Type Hint, Part 1
到目前为止,您只在类型提示中使用了str,float和bool等基本类型。但是Python类型系统非常强大,它可以支持多种更复杂的类型。
在本节中,您将了解有关此类型系统的更多信息,同时实现简单的纸牌游戏。您将看到如何指定:
序列和映射的类型,如元组,列表和字典
键入别名,使代码更容易阅读
该函数和方法不返回任何内容
可以是任何类型的对象
在简要介绍了一些类型理论之后,您将看到更多用Python指定类型的方法。您可以在这里找到代码示例:
https://github.com/realpython/materials/tree/master/python-type-checking
Example: A Deck of Cards
以下示例显示了一副常规纸牌的实现:
game.py
import random
SUITS = " ".split()
RANKS = "2 3 4 5 6 7 8 9 10 J Q K A".split()
def create_deck(shuffle=False):
"""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
def deal_hands(deck):
"""Deal the cards in the deck into four hands"""
return (deck[0::4], deck[1::4], deck[2::4], deck[3::4])
def play():
"""Play a 4-player card game"""
deck = create_deck(shuffle=True)
names = "P1 P2 P3 P4".split()
hands = {n: h for n, h in zip(names, deal_hands(deck))}
for name, cards in hands.items():
card_str = " ".join(f"{s}{r}" for (s, r) in cards)
print(f"{name}: {card_str}")
if name == "main":
play()
每张卡片都表示为套装和等级的字符串元组。卡组表示为卡片列表。create_deck()创建一个由52张扑克牌组成的常规套牌,并可选择随机播放这些牌。deal_hands()将牌组交给四名玩家。
最后,play()扮演游戏。截至目前,它只是通过构建一个洗牌套牌并向每个玩家发牌来准备纸牌游戏。以下是典型输出:
$ python game.py
P4: 9 9 2 7 7 A 6 K 5 6 3 3 Q
P1: A 2 10 J 10 4 5 Q 5 6 A 5 4
P2: 2 7 8 K 3 3 K J A 7 6 10 K
P3: 2 8 8 J Q 9 J 4 8 10 9 4 Q
下面让我一步一步对上面的代码进行拓展。
Sequences and Mappings[序列和映射]
让我们为我们的纸牌游戏添加类型提示。换句话说,让我们注释函数create_deck(),deal_hands()和play()。第一个挑战是你需要注释复合类型,例如用于表示卡片组的列表和用于表示卡片本身的元组。
对于像str、float和bool这样的简单类型,添加类型提示就像使用类型本身一样简单:
name: str = "Guido"
pi: float = 3.142
centered: bool = False
对于复合类型,可以执行相同的操作:
names: list = ["Guido", "Jukka", "Ivan"]
version: tuple = (3, 7, 1)
options: dict = {"centered": False, "capitalize": True}
上面的注释还是不完善,比如names我们只是知道这是list类型,但是我们不知道list里面的元素数据类型
typing模块为我们提供了更精准的定义:
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}
需要注意的是,这些类型中的每一个都以大写字母开头,并且它们都使用方括号来定义项的类型:
names 是一个str类型的list数组。
version 是一个含有3个int类型的元组
options 是一个字典键名类型str,简直类型bool
Typing 还包括其他的很多类型比如 Counter, Deque, FrozenSet, NamedTuple, 和 Set.此外,该模块还包括其他的类型,你将在后面的部分中看到.
让我们回到扑克游戏. 因为卡片是有2个str组成的元组定义的. 所以你可以写作Tuple[str, str],所以函数create_deck()返回值的类型就是 List[Tuple[str, str]].
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
除了返回值之外,您还将bool类型添加到可选的shuffle参数中。
注意: 元组和列表的声明是有区别的
元组是不可变序列,通常由固定数量的可能不同类型的元素组成。例如,我们将卡片表示为套装和等级的元组。通常,您为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]
使用 Sequence 是一个典型的鸭子类型的例子. 也就意味着可以使用len() 和 .
getitem()等方法。
Type Aliases[类型别名]
使用嵌套类型(如卡片组)时,类型提示可能会变得非常麻烦。你可能需要仔细看List [Tuple [str,str]],才能确定它与我们的一副牌是否相符.
现在考虑如何注释deal_hands()函数:
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])
这也太麻烦了!
不怕,我们还可以使用起别名的方式把注解的类型赋值给一个新的变量,方便在后面使用,就像下面这样:
from typing import List, Tuple
Card = Tuple[str, str]
Deck = List[Card]
现在我们就可以使用别名对之前的代码进行注解了:
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]]
当输出Deck的时候可以看到其最终的类型.
函数无返回值时
对于没有返回值的函数,我们可以指定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
作为一个更奇特的情况,请注意您还可以注释从未期望正常返回的函数。这是使用NoReturn完成的:
from typing import NoReturn
def black_hole() -> NoReturn:
raise Exception("There is no going back ...")
因为black_hole()总是引发异常,所以它永远不会正确返回。
Example: Play Some Cards
让我们回到我们的纸牌游戏示例。在游戏的第二个版本中,我们像以前一样向每个玩家发放一张牌。然后选择一个开始玩家并且玩家轮流玩他们的牌。虽然游戏中没有任何规则,所以玩家只会玩随机牌:
game.py
import random
from typing import List, Tuple
SUITS = " ".split()
RANKS = "2 3 4 5 6 7 8 9 10 J Q K A".split()
Card = Tuple[str, str]
Deck = List[Card]
def create_deck(shuffle: bool = False) -> Deck:
"""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
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])
def choose(items):
"""Choose and return a random item"""
return random.choice(items)
def player_order(names, start=None):
"""Rotate player order so that start goes first"""
if start is None:
start = choose(names)
start_idx = names.index(start)
return names[start_idx:] + names[:start_idx]
def play() -> None:
"""Play a 4-player card game"""
deck = create_deck(shuffle=True)
names = "P1 P2 P3 P4".split()
hands = {n: h for n, h in zip(names, deal_hands(deck))}
start_player = choose(names)
turn_order = player_order(names, start=start_player)
# Randomly play cards from each player's hand until empty
while hands[start_player]:
for name in turn_order:
card = choose(hands[name])
hands[name].remove(card)
print(f"{name}: {card[0] + card[1]:<3} ", end="")
print()
if name == "main":
play()
请注意,除了更改play()之外,我们还添加了两个需要类型提示的新函数:choose()和player_order()。在讨论我们如何向它们添加类型提示之前,以下是运行游戏的示例输出:
$ python game.py
P3: 10 P4: 4 P1: 8 P2: Q
P3: 8 P4: 6 P1: 5 P2: K
P3: 9 P4: J P1: A P2: A
P3: Q P4: 3 P1: 7 P2: A
P3: 4 P4: 6 P1: 2 P2: K
P3: K P4: 7 P1: 7 P2: 2
P3: 10 P4: 4 P1: 5 P2: 3
P3: Q P4: K P1: J P2: 9
P3: 2 P4: 4 P1: 9 P2: 10
P3: A P4: 5 P1: J P2: Q
P3: 8 P4: 7 P1: 3 P2: J
P3: 3 P4: 10 P1: 9 P2: 2
P3: 6 P4: 6 P1: 5 P2: 8
在该示例中,随机选择玩家P3作为起始玩家。反过来,每个玩家都会玩一张牌:先是P3,然后是P4,然后是P1,最后是P2。只要手中有任何左手,玩家就会持续打牌。
The Any Type
choose()适用于名称列表和卡片列表(以及任何其他序列)。为此添加类型提示的一种方法是:
import random
from typing import Any, Sequence
def choose(items: Sequence[Any]) -> Any:
return random.choice(items)
这或多或少意味着它:items是一个可以包含任何类型的项目的序列,而choose()将返回任何类型的这样的项目。不是很严谨,此时请考虑以下示例:
choose.py
import random
from typing import Any, Sequence
def choose(items: Sequence[Any]) -> Any:
return random.choice(items)
names = ["Guido", "Jukka", "Ivan"]
reveal_type(names)
name = choose(names)
reveal_type(name)
虽然Mypy会正确推断名称是字符串列表,但由于使用了任意类型,在调用choose ( )后,该信息会丢失:
$ mypy choose.py
choose.py:10: error: Revealed type is 'builtins.list[builtins.str*]'
choose.py:13: error: Revealed type is 'Any'
由此可以得知,如果使用了Any使用mypy的时候将不容易检测。
玩转 Type Hint, Part 2
import random
from typing import Any, Sequence
def choose(items: Sequence[Any]) -> Any:
return random.choice(items)
使用Any的问题在于您不必要地丢失类型信息。您知道如果将一个字符串列表传递给choose(),它将返回一个字符串。
Type Variables[类型声明]
类型声明是一个特殊变量声明,可以采用任何类型,具体取决于具体情况。
让我们创建一个有效封装choose()行为的类型变量:
choose.py
import random
from typing import Sequence, TypeVar
Choosable = TypeVar("Chooseable")
def choose(items: Sequence[Choosable]) -> Choosable:
return random.choice(items)
names = ["Guido", "Jukka", "Ivan"]
reveal_type(names)
name = choose(names)
reveal_type(name)
类型声明必须使用类型模块中的 TypeVar 定义。使用时,类型声明的范围覆盖所有可能的类型,并获取最特定的类型。在这个例子中,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*'
考虑一些其他例子:
choose_examples.py
from choose import choose
reveal_type(choose(["Guido", "Jukka", "Ivan"]))
reveal_type(choose([1, 2, 3]))
reveal_type(choose([True, 42, 3.14]))
reveal_type(choose(["Python", 3, 7])
前两个例子应该有类型str和int,但是后两个呢?单个列表项有不同的类型,在这种情况下,可选择类型变量会尽最大努力适应:
$ mypy choose_examples.py
choose_examples.py:5: error: Revealed type is 'builtins.str*'
choose_examples.py:6: error: Revealed type is 'builtins.int*'
choose_examples.py:7: error: Revealed type is 'builtins.float*'
choose_examples.py:8: error: Revealed type is 'builtins.object*'
正如您已经看到的那样bool是int的子类型,它也是float的子类型。所以在第三个例子中,choose()的返回值保证可以被认为是浮点数。在最后一个例子中,str和int之间没有子类型关系,因此关于返回值可以说最好的是它是一个对象。
请注意,这些示例都没有引发类型错误。有没有办法告诉类型检查器,选择( )应该同时接受字符串和数字,但不能同时接受两者?
您可以通过列出可接受的类型来约束类型变量:
choose.py
import random
from typing import Sequence, TypeVar
Choosable = TypeVar("Choosable", str, float)
def choose(items: Sequence[Choosable]) -> Choosable:
return random.choice(items)
reveal_type(choose(["Guido", "Jukka", "Ivan"]))
reveal_type(choose([1, 2, 3]))
reveal_type(choose([True, 42, 3.14]))
reveal_type(choose(["Python", 3, 7]))
现在Choosable只能是str或float,而Mypy会注意到最后一个例子是一个错误:
$ mypy choose.py
choose.py:11: error: Revealed type is 'builtins.str*'
choose.py:12: error: Revealed type is 'builtins.float*'
choose.py:13: error: Revealed type is 'builtins.float*'
choose.py:14: error: Revealed type is 'builtins.object*'
choose.py:14: error: Value of type variable "Choosable" of "choose"
cannot be "object"
还要注意,在第二个例子中,即使输入列表只包含int对象,该类型也被认为是float类型的。这是因为Choosable仅限于str和float,int是float的一个子类型。
在我们的纸牌游戏中,我们想限制choose()只能用str和Card类型:
Choosable = TypeVar("Choosable", str, Card)
def choose(items: Sequence[Choosable]) -> Choosable:
...
我们简要地提到Sequence表示列表和元组。正如我们所指出的,一个Sequence可以被认为是一个duck类型,因为它可以是实现了. len ()和. getitem ()的任何对象。
鸭子类型和协议
回想一下引言中的以下例子:
def len(obj):
return obj.__len__()
len()方法可以返回任何实现len魔法函数的对象的长度,那我们如何在len()里添加类型提示,尤其是参数obj的类型表示呢?
答案隐藏在学术术语structural subtyping[https://en.wikipedia.org/wiki/Structural_type_system]。structural subtyping的一种方法是根据它们是normal的还是structural的:
在normal系统中,类型之间的比较基于名称和声明。Python类型系统大多是名义上的,因为它们的子类型关系,可以用int来代替float。
在structural系统中,类型之间的比较基于结构。您可以定义一个结构类型“大小”,它包括定义的所有实例。len(),无论其标称类型如何.
目前正在通过PEP 544为Python带来一个成熟的结构类型系统,该系统旨在添加一个称为协议的概念。尽管大多数PEP 544已经在Mypy中实现了。
协议指定了一个或多个实现的方法。例如,所有类定义。len()完成typing.Sized协议。因此,我们可以将len()注释如下:
from typing import Sized
def len(obj: Sized) -> int:
return obj.__len__()
除此之外,在Typing中还包括以下模块 Container, Iterable, Awaitable, 还有 ContextManager.
你也可以声明自定的协议,通过导入typing_extensions模块中的Protocol协议对象,然后写一个继承该方法的子类,像下面这样:
from typing_extensions import Protocol
class Sized(Protocol):
def __len__(self) -> int: ...
def len(obj: Sized) -> int:
return obj.__len__()
需要通过pip安装上面使用的第三方模块
pip install typing-extensions.
Optional 类型
在python中有一种公共模式,就是设置参数的默认值None,这样做通常是为了避免可变默认值的问题,或者让一个标记值标记特殊行为。
在上面的card 例子中, 函数 player_order() 使用 None 作为参数start的默认值,表示还没有指定玩家:
def player_order(names, start=None):
"""Rotate player order so that start goes first"""
if start is None:
start = choose(names)
start_idx = names.index(start)
return names[start_idx:] + names[:start_idx]
这给类型提示带来的挑战是,通常start应该是一个字符串。但是,它也可能采用特殊的非字符串值“None”。
为解决上面的问题,这里可以使用Optional类型:
from typing import Sequence, Optional
def player_order(
names: Sequence[str], start: Optional[str] = None
) -> Sequence[str]:
...
等价于Union类型的 Union[None, str],意思是这个参数的值类型为str,默认的话可以是
请注意,使用Optional或Union时,必须注意变量是否在后面有操作。比如上面的例子通过判断start是否为None。如果不判断None的情况,在做静态类型检查的时候会发生错误:
1 # player_order.py
2
3 from typing import Sequence, Optional
4
5 def player_order(
6 names: Sequence[str], start: Optional[str] = None
7 ) -> Sequence[str]:
8 start_idx = names.index(start)
9 return names[start_idx:] + names[:start_idx]
Mypy告诉你还没有处理start为None的情况。
$ mypy player_order.py
player_order.py:8: error: Argument 1 to "index" of "list" has incompatible
type "Optional[str]"; expected "str"
也可以使用以下操作,声明参数start的类型。
def player_order(names: Sequence[str], start: str = None) -> Sequence[str]:
...
如果你不想 Mypy 出现报错,你可以使用命令
--no-implicit-optional
Example: The Object(ive) of the Game
接下来我们会重写上面的扑克牌游戏,让它看起来更面向对象,以及适当的使用注解。
将我们的纸牌游戏翻译成以下几个类, Card, Deck, Player, Game ,下面是代码实现。
game.py
import random
import sys
class Card:
SUITS = " ".split()
RANKS = "2 3 4 5 6 7 8 9 10 J Q K A".split()
def __init__(self, suit, rank):
self.suit = suit
self.rank = rank
def __repr__(self):
return f"{self.suit}{self.rank}"
class Deck:
def __init__(self, cards):
self.cards = cards
@classmethod
def create(cls, shuffle=False):
"""Create a new deck of 52 cards"""
cards = [Card(s, r) for r in Card.RANKS for s in Card.SUITS]
if shuffle:
random.shuffle(cards)
return cls(cards)
def deal(self, num_hands):
"""Deal the cards in the deck into a number of hands"""
cls = self.__class__
return tuple(cls(self.cards[i::num_hands]) for i in range(num_hands))
class Player:
def __init__(self, name, hand):
self.name = name
self.hand = hand
def play_card(self):
"""Play a card from the player's hand"""
card = random.choice(self.hand.cards)
self.hand.cards.remove(card)
print(f"{self.name}: {card!r:<3} ", end="")
return card
class Game:
def __init__(self, *names):
"""Set up the deck and deal cards to 4 players"""
deck = Deck.create(shuffle=True)
self.names = (list(names) + "P1 P2 P3 P4".split())[:4]
self.hands = {
n: Player(n, h) for n, h in zip(self.names, deck.deal(4))
}
def play(self):
"""Play a card game"""
start_player = random.choice(self.names)
turn_order = self.player_order(start=start_player)
# Play cards from each player's hand until empty
while self.hands[start_player].hand.cards:
for name in turn_order:
self.hands[name].play_card()
print()
def player_order(self, start=None):
"""Rotate player order so that start goes first"""
if start is None:
start = random.choice(self.names)
start_idx = self.names.index(start)
return self.names[start_idx:] + self.names[:start_idx]
if name == "main":
# Read player names from command line
player_names = sys.argv[1:]
game = Game(*player_names)
game.play()
好了,下面让我们添加注解
Type Hints for Methods
方法的类型提示与函数的类型提示非常相似。唯一的区别是self参数不需要注释,因为它是一个类的实例。Card类的类型很容易添加:
class Card:
SUITS = " ".split()
RANKS = "2 3 4 5 6 7 8 9 10 J Q K A".split()
def __init__(self, suit: str, rank: str) -> None:
self.suit = suit
self.rank = rank
def __repr__(self) -> str:
return f"{self.suit}{self.rank}"
注意:init() 的返回值总是为None
Class作为类型
类别和类型之间有对应关系。例如,Card的所有实例一起形成Card类型。要使用类作为类型,只需使用类的名称Card。
例如:Deck(牌组)本质上由一组Card对象组成,你可以像下面这样去声明
class Deck:
def __init__(self, cards: List[Card]) -> None:
self.cards = cards
但是,当您需要引用当前定义的类时,这种方法就不那么有效了。例如,Deck.create() 类方法返回一个带有Deck类型的对象。但是,您不能简单地添加-> Deck,因为Deck类还没有完全定义。
这种情况下可以在注释中使用字符串文字。就像下面使用"Deck",声明了返回类型,然后加入docstring注释进一步说明方法。
class Deck:
@classmethod
def create(cls, shuffle: bool = False) -> "Deck":
"""Create a new deck of 52 cards"""
cards = [Card(s, r) for r in Card.RANKS for s in Card.SUITS]
if shuffle:
random.shuffle(cards)
return cls(cards)
Player类也可以直接使用 Deck作为类型声明. 因为在前面我们已经定义它
class Player:
def __init__(self, name: str, hand: Deck) -> None:
self.name = name
self.hand = hand
通常,注释不会在运行时使用。这为推迟对注释的评估提供了动力。该提议不是将注释评估为Python表达式并存储其值,而是存储注释的字符串表示形式,并仅在需要时对其进行评估。
这种功能计划在Python 4.0中成为标准。但是,在Python 3.7及更高版本中,可以通过导入future属性的annotations来实现:
from future import annotations
class Deck:
@classmethod
def create(cls, shuffle: bool = False) -> Deck:
...
使用 future之后就可以使用Deck对象替换字符串"Deck"了。
返回 self 或者 cls
如前所述,通常不应该注释self或cls参数。在一定程度上,这是不必要的,因为self指向类的实例,所以它将具有类的类型。在Card示例中,self拥有隐式类型Card。此外,显式地添加这种类型会很麻烦,因为还没有定义该类。所以需要使用字符串“Card”声明返回类型。
但是,有一种情况可能需要注释self或cls。考虑如果你有一个其他类继承的超类,并且有返回self或cls的方法会发生什么:
dogs.py
from datetime import date
class Animal:
def __init__(self, name: str, birthday: date) -> None:
self.name = name
self.birthday = birthday
@classmethod
def newborn(cls, name: str) -> "Animal":
return cls(name, date.today())
def twin(self, name: str) -> "Animal":
cls = self.__class__
return cls(name, self.birthday)
class Dog(Animal):
def bark(self) -> None:
print(f"{self.name} says woof!")
fido = Dog.newborn("Fido")
pluto = fido.twin("Pluto")
fido.bark()
pluto.bark()
运行上面的代码,Mypy会抛出下面的错误:
$ mypy dogs.py
dogs.py:24: error: "Animal" has no attribute "bark"
dogs.py:25: error: "Animal" has no attribute "bark"
问题是,即使继承的Dog.newborn()和Dog.twin()方法将返回一个Dog,注释表明它们返回一个Animal。
在这种情况下,您需要更加小心以确保注释正确。返回类型应与self的类型或cls的实例类型匹配。这可以使用TypeVar来完成,这些变量会跟踪实际传递给self和cls的内容:
dogs.py
from datetime import date
from typing import Type, TypeVar
TAnimal = TypeVar("TAnimal", bound="Animal")
class Animal:
def __init__(self, name: str, birthday: date) -> None:
self.name = name
self.birthday = birthday
@classmethod
def newborn(cls: Type[TAnimal], name: str) -> TAnimal:
return cls(name, date.today())
def twin(self: TAnimal, name: str) -> TAnimal:
cls = self.__class__
return cls(name, self.birthday)
class Dog(Animal):
def bark(self) -> None:
print(f"{self.name} says woof!")
fido = Dog.newborn("Fido")
pluto = fido.twin("Pluto")
fido.bark()
pluto.bark()
在这个例子中有几个需要注意的点:
类型变量TAnimal用于表示返回值可能是Animal的子类的实例。.
我们指定Animal是TAnimal的上限。指定绑定意味着TAnimal将是Animal子类之一。这可以正确限制所允许的类型。
typing.Type []是type()的类型。需要注意,是cls的类方法需要使用这种形式注解,而self就不用使用。
注解 *args 和 **kwargs
在面向对象的游戏版本中,我们添加了在命令行上命名玩家的选项。这是通过在程序名称后面列出玩家名称来完成的:
$ python game.py GeirArne Dan Joanna
Dan: A Joanna: 9 P1: A GeirArne: 2
Dan: A Joanna: 6 P1: 4 GeirArne: 8
Dan: K Joanna: Q P1: K GeirArne: 5
Dan: 2 Joanna: J P1: 7 GeirArne: K
Dan: 10 Joanna: 3 P1: 4 GeirArne: 8
Dan: 6 Joanna: Q P1: Q GeirArne: J
Dan: 2 Joanna: 4 P1: 8 GeirArne: 7
Dan: 10 Joanna: 3 P1: 3 GeirArne: 2
Dan: K Joanna: 5 P1: 7 GeirArne: J
Dan: 6 Joanna: 9 P1: J GeirArne: 10
Dan: 3 Joanna: 5 P1: 9 GeirArne: Q
Dan: A Joanna: 9 P1: 10 GeirArne: 8
Dan: 6 Joanna: 5 P1: 7 GeirArne: 4
关于类型注释:即使名称是字符串元组,也应该只注释每个名称的类型。换句话说,您应该使用字符串而不是元组[字符串],就像下面这个例子:
class Game:
def __init__(self, *names: str) -> None:
"""Set up the deck and deal cards to 4 players"""
deck = Deck.create(shuffle=True)
self.names = (list(names) + "P1 P2 P3 P4".split())[:4]
self.hands = {
n: Player(n, h) for n, h in zip(self.names, deck.deal(4))
}
类似地,如果有一个接受**kwargs的函数或方法,那么你应该只注释每个可能的关键字参数的类型。
Callables可调用类型
函数是Python中的一类对象。可以使用函数作为其他函数的参数。这意味着需要能够添加表示函数的类型提示。
函数以及lambdas、方法和类都由type的Callable对象表示。参数的类型和返回值通常也表示。例如,Callable[[A1, A2, A3],Rt]表示一个函数,它有三个参数,分别具有A1、A2和A3类型。函数的返回类型是Rt。
在下面这个例子, 函数 do_twice() 传入一个Callable类型的func参数,并指明传入的函数的参数类型为str,返回值类型为str。比如传入参数create_greeting.
do_twice.py
from typing import Callable
def do_twice(func: Callable[[str], str], argument: str) -> None:
print(func(argument))
print(func(argument))
def create_greeting(name: str) -> str:
return f"Hello {name}"
do_twice(create_greeting, "Jekyll")
Example: Hearts
让我们以红心游戏的完整例子来结束。您可能已经从其他计算机模拟中了解了这个游戏。下面是对规则的简要回顾:
四名玩家每人玩13张牌。
持有2的玩家开始第一轮,必须出2。
如果可能的话,玩家轮流打牌,跟随领头的一套牌。
在第一套牌中打出最高牌的玩家赢了这个把戏,并在下一个回合中成为开始牌的玩家。
玩家不能用,除非已经在之前的技巧中玩过。
玩完所有牌后,玩家如果拿到某些牌就会获得积分:
Q为13分
每个1为分
一场比赛持续几轮,直到得到100分以上。得分最少的玩家获胜
具体游戏规则可以网上搜索一下.
在这个示例中,没有多少新的类型概念是尚未见过的。因此,我们将不详细讨论这段代码,而是将其作为带注释代码的示例。