点击上方蓝字关注
正文共:30429 字
预计阅读时间:76分钟
原文链接:https://realpython.com/python-type-checking/
作者:Geir Arne Hjelle
译者:陈祥安
在本指南中,你将了解Python类型检查。传统上,Python解释器以灵活但隐式的方式处理类型。Python的最新版本允许你指定可由不同工具使用的显式类型提示,以帮助您更有效地开发代码。
通过本教程,你将学到以下内容:
类型注解和提示(Type annotations and type hints)
代码里添加静态类型
静态类型检查
运行时强制类型一致
这是一个全面的指南,将涵盖很多领域。如果您只是想快速了解一下类型提示在Python中是如何工作的,并查看类型检查是否包括在您的代码中,那么您不需要阅读全部内容。Hello Types和正反两部分将让您大致了解类型检查是如何工作的,并介绍它在什么时候有用。
Type Systems
所有的编程语言都包括某种类型的系统,该系统将它可以处理的对象类别以及如何处理这些类别形式化。例如,类型系统可以定义一个数字类型,其中42是数字类型对象的一个例子。
动态类型
Python是一种动态类型语言。这意味着Python解释器仅在代码运行时进行类型检查,并且允许变量的类型在其生命周期内进行更改。以下示例演示了Python具有动态类型:
False:
1 + "two" # This line never runs, so no TypeError is raised
:
1 + 2
...
3
"two" # Now this is type checked, and a TypeError is raised +
TypeError: unsupported operand type(s) for +: \'int\' and \'str\'
在上面例子中,if从未运行过,因此它未被类型检查过。else部分,当计算1 +“2”时,因为类型不一致所以,会产生一个类型错误。
如果改变一个变量的值的类型
"Hello" > thing =
> type(thing)
<class \'str\'>
28.1 > thing =
> type(thing)
<class \'float\'>
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围绕字符串:
"python type checking")) print(headline(
Python Type Checking
--------------------
"python type checking", align=False)) print(headline(
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
类型提示可帮助您构建和维护更清晰的体系结构。编写类型提示的行为迫使您考虑程序中的类型。虽然Python的动态特性是其重要资产之一,但是有意识地依赖于鸭子类型,重载方法或多种返回类型是一件好事。
需要注意的是,类型提示会在启动时带来轻微的损失。如果您需要使用类型模块,那么导入时间可能很长,尤其是在简短的脚本中。
那么,您应该在自己的代码中使用静态类型检查吗?这不是一个全有或全无的问题。幸运的是,Python支持渐进式输入的概念。这意味着您可以逐渐在代码中引入类型。没有类型提示的代码将被静态类型检查器忽略。因此,您可以开始向关键组件添加类型,只要它能为您增加价值,就可以继续。
关于是否向项目添加类型的一些经验法则:
如果您刚开始学习Python,可以安全地等待类型提示,直到您有更多经验。
类型提示在短暂抛出脚本中增加的价值很小。
在其他人使用的库中,尤其是在PyPI上发布的库中,类型提示会增加很多价值。使用库的其他代码需要这些类型提示才能正确地进行类型检查。
在较大的项目中,类型提示可以帮助您理解类型是如何在代码中流动的,强烈建议您这样做。在与他人合作的项目中更是如此。
Bernat Gabor在他的文章《Python中类型提示的状态》中建议,只要值得编写单元测试,就应该使用类型提示。实际上,类型提示在代码中扮演着类似于测试的角色:它们帮助开发人员编写更好的代码。
注解
Python 3.0中引入了注释,最初没有任何特定用途。它们只是将任意表达式与函数参数和返回值相关联的一种方法。
多年以后,PEP 484根据Jukka Lehtosalo博士项目Mypy所做的工作,定义了如何向Python代码添加类型提示。添加类型提示的主要方法是使用注释。随着类型检查变得越来越普遍,这也意味着注释应该主要保留给类型提示。
接下来的章节将解释注释如何在类型提示的上下文中工作。
函数注解
之前我们也提到过函数的注解例子向下面这样:
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__魔法函数可以输出函数的注解信息。
1.23) > circumference(
7.728317927830891
_ > circumference.__annotations_
{\'radius\': <class \'float\'>, \'return\':
float \'>}
有时您可能会对Mypy如何解释您的类型提示感到困惑。对于这些情况,有一些特殊的Mypy表达式:reveal type()和reveal local()。您可以在运行Mypy之前将这些添加到您的代码中,Mypy将报告它所推断的类型。例如,将以下代码保存为reveal.py。
# reveal.py
import math
reveal_type(math.pi)
1 =
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
变量注解
有时类型检查器也需要帮助来确定变量的类型。变量注释在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__字典中::
1) > circumference(
6.284
_ > __annotations_
{\'pi\': <class \'float\'>}
即使只是定义变量没有给赋值,也可以通过__annotations__获取其类型。虽然
在python中没有赋值的变量直接输出是错误的。
nothing: str >
> nothing
NameError: name \'nothing\' is not defined
_ > __annotations_
{\'nothing\': <class \'str\'>}
类型注解
如上所述,注释是在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中使用,代价是必须维护第二组文件。通常,如果无法更改原始源代码,则只需使用存根文件。
Playing With Python Types, Part 1
在本节中,您将了解有关此类型系统的更多信息,同时实现简单的纸牌游戏。您将看到如何指定:
序列和映射的类型,如元组,列表和字典
键入别名,使代码更容易阅读
该函数和方法不返回任何内容
可以是任何类型的对象
在简要介绍了一些类型理论之后,您将看到更多用Python指定类型的方法。您可以在这里找到代码示例:https://github.com/realpython/materials/tree/master/python-type-checking
以下示例显示了一副常规纸牌的实现:
# 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
下面让我一步一步对上面的代码进行拓展。
让我们为我们的纸牌游戏添加类型提示。换句话说,让我们注释函数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模块为我们提供了更精准的定义:
import Dict, List, Tuple typing
"Guido", "Jukka", "Ivan"] names: List[str] = [
3, 7, 1) version: Tuple[int, int, int] = (
"centered": False, "capitalize": True} options: Dict[str, bool] = {
需要注意的是,这些类型中的每一个都以大写字母开头,并且它们都使用方括号来定义项的类型:
names
是一个str类型的list数组。
version
是一个含有3个int类型的元组
options
是一个字典键名类型str,简直类型bool
typing
还包括其他的很多类型比如 Counter
, Dequ
e
, 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元组编写元组[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__()等方法。
使用嵌套类型(如卡片组)时,类型提示可能会变得非常麻烦。你可能需要仔细看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( )总是引发异常,所以它永远不会正确返回。
让我们回到我们的纸牌游戏示例。在游戏的第二个版本中,我们像以前一样向每个玩家发放一张牌。然后选择一个开始玩家并且玩家轮流玩他们的牌。虽然游戏中没有任何规则,所以玩家只会玩随机牌:
# 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。只要手中有任何左手,玩家就会持续打牌。
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的时候将不容易检测。
Playing With Python Types, Part 2
import random
from typing import Any, Sequence
def choose(items: Sequence[Any]) -> Any:
return random.choice(items)
使用Any的问题在于您不必要地丢失类型信息。您知道如果将一个字符串列表传递给choose(),它将返回一个字符串。
类型变量
类型变量是一个特殊变量,可以采用任何类型,具体取决于具体情况。
让我们创建一个有效封装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背后。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
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作为类型
例如: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或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可调用类型
函数以及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分以上。得分最少的玩家获胜
具体游戏规则可以网上搜索一下.
在这个示例中,没有多少新的类型概念是尚未见过的。因此,我们将不详细讨论这段代码,而是将其作为带注释代码的示例。
# hearts.py
from collections import Counter
import random
import sys
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
from typing import overload
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 value(self) -> int:
"""The value of a card is rank as a number"""
return self.RANKS.index(self.rank)
def points(self) -> int:
"""Points this card is worth"""
if self.suit == "♠" and self.rank == "Q":
return 13
if self.suit == "♡":
return 1
return 0
def __eq__(self, other: Any) -> Any:
return self.suit == other.suit and self.rank == other.rank
def __lt__(self, other: Any) -> Any:
return self.value < other.value
def __repr__(self) -> str:
return f"{self.suit}{self.rank}"
class Deck(Sequence[Card]):
def __init__(self, cards: List[Card]) -> None:
self.cards = cards
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)
def play(self, card: Card) -> None:
"""Play one card by removing it from the deck"""
self.cards.remove(card)
def deal(self, num_hands: int) -> Tuple["Deck", ...]:
"""Deal the cards in the deck into a number of hands"""
return tuple(self[i::num_hands] for i in range(num_hands))
def add_cards(self, cards: List[Card]) -> None:
"""Add a list of cards to the deck"""
self.cards += cards
def __len__(self) -> int:
return len(self.cards)
def __getitem__(self, key: int) -> Card: ...
def __getitem__(self, key: slice) -> "Deck": ...
def __getitem__(self, key: Union[int, slice]) -> Union[Card, "Deck"]:
if isinstance(key, int):
return self.cards[key]
elif isinstance(key, slice):
cls = self.__class__
return cls(self.cards[key])
else:
raise TypeError("Indices must be integers or slices")
def __repr__(self) -> str:
return " ".join(repr(c) for c in self.cards)
class Player:
def __init__(self, name: str, hand: Optional[Deck] = None) -> None:
self.name = name
self.hand = Deck([]) if hand is None else hand
def playable_cards(self, played: List[Card], hearts_broken: bool) -> Deck:
"""List which cards in hand are playable this round"""
if Card("♣", "2") in self.hand:
return Deck([Card("♣", "2")])
lead = played[0].suit if played else None
playable = Deck([c for c in self.hand if c.suit == lead]) or self.hand
if lead is None and not hearts_broken:
playable = Deck([c for c in playable if c.suit != "♡"])
return playable or Deck(self.hand.cards)
def non_winning_cards(self, played: List[Card], playable: Deck) -> Deck:
"""List playable cards that are guaranteed to not win the trick"""
if not played:
return Deck([])
lead = played[0].suit
best_card = max(c for c in played if c.suit == lead)
return Deck([c for c in playable if c < best_card or c.suit != lead])
def play_card(self, played: List[Card], hearts_broken: bool) -> Card:
"""Play a card from a cpu player\'s hand"""
playable = self.playable_cards(played, hearts_broken)
non_winning = self.non_winning_cards(played, playable)
# Strategy
if non_winning:
# Highest card not winning the trick, prefer points
card = max(non_winning, key=lambda c: (c.points, c.value))
elif len(played) < 3:
# Lowest card maybe winning, avoid points
card = min(playable, key=lambda c: (c.points, c.value))
else:
# Highest card guaranteed winning, avoid points
card = max(playable, key=lambda c: (-c.points, c.value))
self.hand.cards.remove(card)
print(f"{self.name} -> {card}")
return card
def has_card(self, card: Card) -> bool:
return card in self.hand
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.name!r}, {self.hand})"
class HumanPlayer(Player):
def play_card(self, played: List[Card], hearts_broken: bool) -> Card:
"""Play a card from a human player\'s hand"""
playable = sorted(self.playable_cards(played, hearts_broken))
p_str = " ".join(f"{n}: {c}" for n, c in enumerate(playable))
np_str = " ".join(repr(c) for c in self.hand if c not in playable)
print(f" {p_str} (Rest: {np_str})")
while True:
try:
card_num = int(input(f" {self.name}, choose card: "))
card = playable[card_num]
except (ValueError, IndexError):
pass
else:
break
self.hand.play(card)
print(f"{self.name} => {card}")
return card
class HeartsGame:
def __init__(self, *names: str) -> None:
self.names = (list(names) + "P1 P2 P3 P4".split())[:4]
self.players = [Player(n) for n in self.names[1:]]
self.players.append(HumanPlayer(self.names[0]))
def play(self) -> None:
"""Play a game of Hearts until one player go bust"""
score = Counter({n: 0 for n in self.names})
while all(s < 100 for s in score.values()):
print("\nStarting new round:")
round_score = self.play_round()
score.update(Counter(round_score))
print("Scores:")
for name, total_score in score.most_common(4):
print(f"{name:<15} {round_score[name]:>3} {total_score:>3}")
winners = [n for n in self.names if score[n] == min(score.values())]
print(f"\n{\' and \'.join(winners)} won the game")
def play_round(self) -> Dict[str, int]:
"""Play a round of the Hearts card game"""
deck = Deck.create(shuffle=True)
for player, hand in zip(self.players, deck.deal(4)):
player.hand.add_cards(hand.cards)
start_player = next(
p for p in self.players if p.has_card(Card("♣", "2"))
)
tricks = {p.name: Deck([]) for p in self.players}
hearts = False
# Play cards from each player\'s hand until empty
while start_player.hand:
played: List[Card] = []
turn_order = self.player_order(start=start_player)
for player in turn_order:
card = player.play_card(played, hearts_broken=hearts)
played.append(card)
start_player = self.trick_winner(played, turn_order)
tricks[start_player.name].add_cards(played)
print(f"{start_player.name} wins the trick\n")
hearts = hearts or any(c.suit == "♡" for c in played)
return self.count_points(tricks)
def player_order(self, start: Optional[Player] = None) -> List[Player]:
"""Rotate player order so that start goes first"""
if start is None:
start = random.choice(self.players)
start_idx = self.players.index(start)
return self.players[start_idx:] + self.players[:start_idx]
def trick_winner(trick: List[Card], players: List[Player]) -> Player:
lead = trick[0].suit
valid = [
(c.value, p) for c, p in zip(trick, players) if c.suit == lead
]
return max(valid)[1]
def count_points(tricks: Dict[str, Deck]) -> Dict[str, int]:
return {n: sum(c.points for c in cards) for n, cards in tricks.items()}
if __name__ == "__main__":
# Read player names from the command line
player_names = sys.argv[1:]
game = HeartsGame(*player_names)
game.play()
对于上面的代码有几个注意点:
对于难以使用Union或类型变量表达的类型关系比如魔法函数,可以使用@overload装饰器。
子类对应于子类型,因此可以在任何需要玩家的地方使用HumanPlayer。
当子类从超类重新实现方法时,类型注释必须匹配。有关示例,请参阅HumanPlayer.play_card()。
开始游戏时,你控制第一个玩家。输入数字以选择要玩的牌。下面是一个游戏的例子,突出显示的线条显示了玩家的选择:
python hearts.py GeirArne Aldren Joanna Brad
Starting new round:
Brad -> ♣2
0: ♣5 1: ♣Q 2: ♣K (Rest: ♢6 ♡10 ♡6 ♠J ♡3 ♡9 ♢10 ♠7 ♠K ♠4)
choose card: 2
GeirArne => ♣K
Aldren -> ♣10
Joanna -> ♣9
GeirArne wins the trick
0: ♠4 1: ♣5 2: ♢6 3: ♠7 4: ♢10 5: ♠J 6: ♣Q 7: ♠K (Rest: ♡10 ♡6 ♡3 ♡9)
choose card: 0
GeirArne => ♠4
Aldren -> ♠5
Joanna -> ♠3
Brad -> ♠2
Aldren wins the trick
...
Joanna -> ♡J
Brad -> ♡2
0: ♡6 1: ♡9 (Rest: )
choose card: 1
GeirArne => ♡9
Aldren -> ♡A
Aldren wins the trick
Aldren -> ♣A
Joanna -> ♡Q
Brad -> ♣J
0: ♡6 (Rest: )
choose card: 0
GeirArne => ♡6
Aldren wins the trick
Scores:
Brad 14 14
Aldren 10 10
GeirArne 1 1
Joanna 1 1
当前目前所有的typing方法的使用场景就结束了。觉得有用的朋友可以点个已看,或者转发到朋友圈分享更更多好友。
爬虫实战之puppeteer破解阿里h5滑动验证码
python下载夏目友人帳
python小应用之整理手机图片
这顶海贼王的帽子,我Python给你带上了 | 【人脸识别应用】
一个有趣的小例子,带你入门协程模块-asyncio
致转行AI的在校大学生的一封信
转行AI需要看的一些文章
转行学AI,具体细分方向如何选,来自一线工程师的感悟
用法律武器,痛击腾讯侵权行为!!!湾区人工智能可以改善知识产权现状吗?
【送书PDF】Python编程从入门到实践
Python从入门到精通,深度学习与机器学习资料大礼包!
【免费】某机构最新3980元机器学习/大数据课程高速下载,限量200份