最新python类型检测最终指南--Typing的使用_typing_extensions,2024年最新滴滴 算法 面试

最后

🍅 硬核资料:关注即可领取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 还包括其他的很多类型比如 CounterDequ``eFrozenSetNamedTuple, 和 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中还包括以下模块 ContainerIterableAwaitable, 还有 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

接下来我们会重写上面的扑克牌游戏,让它看起来更面向对象,以及适当的使用注解。

将我们的纸牌游戏翻译成以下几个类, CardDeckPlayerGame ,下面是代码实现。​​​​​​​

# 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行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 25
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值