最后
🍅 硬核资料:关注即可领取PPT模板、简历模板、行业经典书籍PDF。
🍅 技术互助:技术群大佬指点迷津,你的问题可能不是问题,求资源在群里喊一声。
🍅 面试题库:由技术群里的小伙伴们共同投稿,热乎的大厂面试真题,持续更新中。
🍅 知识体系:含编程语言、算法、大数据生态圈组件(Mysql、Hive、Spark、Flink)、数据仓库、Python、前端等等。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
$ mypy headlines.py
$
然后就可以打印结果了
$ python headlines.pyPython Type Checking--------------------oooooooooooooooooooo Use Mypy oooooooooooooooooooo
第一个标题与左侧对齐,而第二个标题居中。
Pros and Cons
类型提示的增加方便了IDE的代码提示功能,我们看到下面text使用.即可得到str使用的一些方法和熟悉。
类型提示可帮助您构建和维护更清晰的体系结构。编写类型提示的行为迫使您考虑程序中的类型。虽然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__魔法函数可以输出函数的注解信息。
>>> circumference(1.23)7.728317927830891
>>> circumference.__annotations__{'radius': <class 'float'>, 'return': <class 'float'>}
有时您可能会对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.pyreveal.py:4: error: Revealed type is 'builtins.float'
reveal.py:8: error: Revealed local types are:reveal.py:8: error: circumference: builtins.floatreveal.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__字典中::
>>> circumference(1)6.284
>>> __annotations__{'pi': <class 'float'>}
即使只是定义变量没有给赋值,也可以通过__annotations__获取其类型。虽然
在python中没有赋值的变量直接输出是错误的。
>>> nothing: str>>> nothingNameError: 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.pyheadline.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
到目前为止,您只在类型提示中使用了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.pyP4: ♣9 ♢9 ♡2 ♢7 ♡7 ♣A ♠6 ♡K ♡5 ♢6 ♢3 ♣3 ♣QP1: ♡A ♠2 ♠10 ♢J ♣10 ♣4 ♠5 ♡Q ♢5 ♣6 ♠A ♣5 ♢4P2: ♢2 ♠7 ♡8 ♢K ♠3 ♡3 ♣K ♠J ♢A ♣7 ♡6 ♡10 ♠KP3: ♣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
, 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]
>>> Decktyping.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.pyplay.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.pyP3: ♢10 P4: ♣4 P1: ♡8 P2: ♡QP3: ♣8 P4: ♠6 P1: ♠5 P2: ♡KP3: ♢9 P4: ♡J P1: ♣A P2: ♡AP3: ♠Q P4: ♠3 P1: ♠7 P2: ♠AP3: ♡4 P4: ♡6 P1: ♣2 P2: ♠KP3: ♣K P4: ♣7 P1: ♡7 P2: ♠2P3: ♣10 P4: ♠4 P1: ♢5 P2: ♡3P3: ♣Q P4: ♢K P1: ♣J P2: ♡9P3: ♢2 P4: ♢4 P1: ♠9 P2: ♠10P3: ♢A P4: ♡5 P1: ♠J P2: ♢QP3: ♠8 P4: ♢7 P1: ♢3 P2: ♢JP3: ♣3 P4: ♡10 P1: ♣9 P2: ♡2P3: ♢6 P4: ♣6 P1: ♣5 P2: ♢8
在该示例中,随机选择玩家P3作为起始玩家。反过来,每个玩家都会玩一张牌:先是P3,然后是P4,然后是P1,最后是P2。只要手中有任何左手,玩家就会持续打牌。
The Any
Type
choose()适用于名称列表和卡片列表(以及任何其他序列)。为此添加类型提示的一种方法是:
import randomfrom 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.pychoose.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 randomfrom 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.pychoose.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.pychoose_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.pychoose.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.pyplayer_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()
好了,下面让我们添加注解
做了那么多年开发,自学了很多门编程语言,我很明白学习资源对于学一门新语言的重要性,这些年也收藏了不少的Python干货,对我来说这些东西确实已经用不到了,但对于准备自学Python的人来说,或许它就是一个宝藏,可以给你省去很多的时间和精力。
别在网上瞎学了,我最近也做了一些资源的更新,只要你是我的粉丝,这期福利你都可拿走。
我先来介绍一下这些东西怎么用,文末抱走。
(1)Python所有方向的学习路线(新版)
这是我花了几天的时间去把Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。
最近我才对这些路线做了一下新的更新,知识体系更全面了。
(2)Python学习视频
包含了Python入门、爬虫、数据分析和web开发的学习视频,总共100多个,虽然没有那么全面,但是对于入门来说是没问题的,学完这些之后,你可以按照我上面的学习路线去网上找其他的知识资源进行进阶。
(3)100多个练手项目
我们在看视频学习的时候,不能光动眼动脑不动手,比较科学的学习方法是在理解之后运用它们,这时候练手项目就很适合了,只是里面的项目比较多,水平也是参差不齐,大家可以挑自己能做的项目去练练。
(4)200多本电子书
这些年我也收藏了很多电子书,大概200多本,有时候带实体书不方便的话,我就会去打开电子书看看,书籍可不一定比视频教程差,尤其是权威的技术书籍。
基本上主流的和经典的都有,这里我就不放图了,版权问题,个人看看是没有问题的。
(5)Python知识点汇总
知识点汇总有点像学习路线,但与学习路线不同的点就在于,知识点汇总更为细致,里面包含了对具体知识点的简单说明,而我们的学习路线则更为抽象和简单,只是为了方便大家只是某个领域你应该学习哪些技术栈。
(6)其他资料
还有其他的一些东西,比如说我自己出的Python入门图文类教程,没有电脑的时候用手机也可以学习知识,学会了理论之后再去敲代码实践验证,还有Python中文版的库资料、MySQL和HTML标签大全等等,这些都是可以送给粉丝们的东西。
这些都不是什么非常值钱的东西,但对于没有资源或者资源不是很好的学习者来说确实很不错,你要是用得到的话都可以直接抱走,关注过我的人都知道,这些都是可以拿到的。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!