python類型檢測最終指南--Typing模塊的使用

正文共:30429 字javascript

預計閱讀時間:76分鐘css

原文連接:https://realpython.com/python-type-checking/java

做者:Geir Arne Hjelle python

譯者:陳祥安nginx

 

在本指南中,你將瞭解Python類型檢查。傳統上,Python解釋器以靈活但隱式的方式處理類型。Python的最新版本容許你指定可由不一樣工具使用的顯式類型提示,以幫助您更有效地開發代碼。git

經過本教程,你將學到如下內容:github

  • 類型註解和提示(Type annotations and type hints)sql

  • 代碼裏添加靜態類型shell

  • 靜態類型檢查編程

  • 運行時強制類型一致

     

這是一個全面的指南,將涵蓋不少領域。若是您只是想快速瞭解一下類型提示在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 raisedTypeError: unsupported operand type(s) for +: 'int' and 'str'

在上面例子中,if從未運行過,所以它未被類型檢查過。else部分,當計算1 +「2」時,由於類型不一致因此,會產生一個類型錯誤。

若是改變一個變量的值的類型

>>> thing = "Hello">>> type(thing)<class 'str'>
>>> thing = 28.1>>> 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圍繞字符串:

>>> 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.pyheadlines.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.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 還包括其餘的不少類型好比 CounterDequeFrozenSetNamedTuple, 和 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()

好了,下面讓咱們添加註解

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.pydogs.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 datefrom 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 JoannaDan: ♢A Joanna: ♡9 P1: ♣A GeirArne: ♣2Dan: ♡A Joanna: ♡6 P1: ♠4 GeirArne: ♢8Dan: ♢K Joanna: ♢Q P1: ♣K GeirArne: ♠5Dan: ♡2 Joanna: ♡J P1: ♠7 GeirArne: ♡KDan: ♢10 Joanna: ♣3 P1: ♢4 GeirArne: ♠8Dan: ♣6 Joanna: ♡Q P1: ♣Q GeirArne: ♢JDan: ♢2 Joanna: ♡4 P1: ♣8 GeirArne: ♡7Dan: ♡10 Joanna: ♢3 P1: ♡3 GeirArne: ♠2Dan: ♠K Joanna: ♣5 P1: ♣7 GeirArne: ♠JDan: ♠6 Joanna: ♢9 P1: ♣J GeirArne: ♣10Dan: ♠3 Joanna: ♡5 P1: ♣9 GeirArne: ♠QDan: ♠A Joanna: ♠9 P1: ♠10 GeirArne: ♡8Dan: ♢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]表示一個函數,它有三個參數,分別具備A一、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 Counterimport randomimport sysfrom typing import Any, Dict, List, Optional, Sequence, Tuple, Unionfrom 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
@property def value(self) -> int: """The value of a card is rank as a number""" return self.RANKS.index(self.rank)
@property 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
@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)
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)
@overload def __getitem__(self, key: int) -> Card: ...
@overload 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]
@staticmethod 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]
@staticmethod 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) GeirArne, choose card: 2GeirArne => ♣KAldren -> ♣10Joanna -> ♣9GeirArne wins the trick
0: ♠4 1: ♣5 2: ♢6 3: ♠7 4: ♢10 5: ♠J 6: ♣Q 7: ♠K (Rest: ♡10 ♡6 ♡3 ♡9) GeirArne, choose card: 0GeirArne => ♠4Aldren -> ♠5Joanna -> ♠3Brad -> ♠2Aldren wins the trick
...
Joanna -> ♡JBrad -> ♡2 0: ♡6 1: ♡9 (Rest: ) GeirArne, choose card: 1GeirArne => ♡9Aldren -> ♡AAldren wins the trick
Aldren -> ♣AJoanna -> ♡QBrad -> ♣J 0: ♡6 (Rest: ) GeirArne, choose card: 0GeirArne => ♡6Aldren wins the trick
Scores:Brad 14 14Aldren 10 10GeirArne 1 1Joanna 1 1

當前目前全部的typing方法的使用場景就結束了。以爲有用的朋友能夠點個已看,或者轉發到朋友圈分享更更多好友。

 

猜你喜歡

爬蟲實戰之puppeteer破解阿里h5滑動驗證碼

python下載夏目友人帳

python小應用之整理手機圖片

這頂海賊王的帽子,我Python給你帶上了 | 【人臉識別應用】

一個有趣的小例子,帶你入門協程模塊-asyncio

相關文章
相關標籤/搜索