Python Type Hint類型注解



原文地址:https://realpython.com/python-type-checking/

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

通過本教程,你將學到以下內容:

  • 類型注解和提示(Type annotations and type hints)
  • 代碼里添加靜態類型
  • 靜態類型檢查
  • 運行時強制類型一致

這是一個全面的指南,將涵蓋很多領域。如果您只是想快速了解一下類型提示在Python中是如何工作的,並查看類型檢查是否包括在您的代碼中,那么您不需要閱讀全部內容。Hello Types和正反兩部分將讓您大致了解類型檢查是如何工作的,並介紹它在什么時候有用。

Type Systems

所有的編程語言都包括某種類型的系統,該系統將它可以處理的對象類別以及如何處理這些類別形式化。例如,類型系統可以定義一個數字類型,其中42是數字類型對象的一個例子。

動態類型

Python是一種動態類型語言。這意味着Python解釋器僅在代碼運行時進行類型檢查,並且允許變量的類型在其生命周期內進行更改。以下示例演示了Python具有動態類型:

>>> if False:
...     1 + "two"  # This line never runs, so no TypeError is raised
... else:
...     1 + 2
...
3

>>> 1 + "two"  # Now this is type checked, and a TypeError is raised
TypeError: unsupported operand type(s) for +: 'int' and 'str'

在上面例子中,if從未運行過,因此它未被類型檢查過。成功運行了else部分得到結果3,緊接着下面計算1 +“2”時,因為類型不一致所以,產生一個類型錯誤。

看下一個例子,如果改變一個變量的值的類型

>>> thing = "Hello"
>>> type(thing)
<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.py
headlines.py:10: error: Argument "align" to "headline" has incompatible
                        type "str"; expected "bool"

根據類型提示,Mypy能夠告訴我們我們在第10行使用了錯誤的類型

這樣說明一個問題參數名align不是很好確定參數是bool類型,我們將代碼改成下面這樣,換一個識別度高的參數名centered。

  # headlines.py
 
  def headline(text: str, centered: bool = False):
      if not centered:
          return f"{text.title()}\n{'-' * len(text)}"
      else:
          return f" {text.title()} ".center(50, "o")
 
  print(headline("python type checking"))
  print(headline("use mypy", centered=True))

再次運行文件發現沒有錯誤提示,ok。

$ mypy headlines.py
$ 

然后就可以打印結果了

$ python headlines.py
Python Type Checking
--------------------
oooooooooooooooooooo Use Mypy oooooooooooooooooooo

第一個標題與左側對齊,而第二個標題居中。

Pros and Cons

類型提示的增加方便了IDE的代碼提示功能,我們看到下面text使用.即可得到str使用的一些方法和熟悉。
類型提示可幫助您構建和維護更清晰的體系結構。編寫類型提示的行為迫使您考慮程序中的類型。雖然Python的動態特性是其重要資產之一,但是有意識地依賴於鴨子類型,重載方法或多種返回類型是一件好事。

需要注意的是,類型提示會在啟動時帶來輕微的損失。如果您需要使用類型模塊,那么導入時間可能很長,尤其是在簡短的腳本中。

那么,您應該在自己的代碼中使用靜態類型檢查嗎?這不是一個全有或全無的問題。幸運的是,Python支持漸進式輸入的概念。這意味着您可以逐漸在代碼中引入類型。沒有類型提示的代碼將被靜態類型檢查器忽略。因此,您可以開始向關鍵組件添加類型,只要它能為您增加價值,就可以繼續。

關於是否向項目添加類型的一些經驗法則:
如果您剛開始學習Python,可以安全地等待類型提示,直到您有更多經驗。

類型提示在短暫拋出腳本中增加的價值很小。
在其他人使用的庫中,尤其是在PyPI上發布的庫中,類型提示會增加很多價值。使用庫的其他代碼需要這些類型提示才能正確地進行類型檢查。

在較大的項目中,類型提示可以幫助您理解類型是如何在代碼中流動的,強烈建議您這樣做。在與他人合作的項目中更是如此。

Bernat Gabor在他的文章《Python中類型提示的狀態》中建議,只要值得編寫單元測試,就應該使用類型提示。實際上,類型提示在代碼中扮演着類似於測試的角色:它們幫助開發人員編寫更好的代碼。

Annotations[注解]

Python 3.0中引入了注釋,最初沒有任何特定用途。它們只是將任意表達式與函數參數和返回值相關聯的一種方法。

多年以后,PEP 484根據Jukka Lehtosalo博士項目Mypy所做的工作,定義了如何向Python代碼添加類型提示。添加類型提示的主要方法是使用注釋。隨着類型檢查變得越來越普遍,這也意味着注釋應該主要保留給類型提示。

接下來的章節將解釋注釋如何在類型提示的上下文中工作。

Function Annotations[函數注解]

之前我們也提到過函數的注解例子向下面這樣:

def func(arg: arg_type, optarg: arg_type = default) -> return_type:
    ...

對於參數,語法是參數:注釋,而返回類型使用->注釋進行注釋。請注意,注釋必須是有效的Python表達式。

以下簡單示例向計算圓周長的函數添加注釋:

import math

def circumference(radius: float) -> float:
    return 2 * math.pi * radius

通調用circumference對象的__annotations__魔法函數可以輸出函數的注解信息。

>>> circumference(1.23)
7.728317927830891

>>> circumference.__annotations__
{'radius': <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.py
reveal.py:4: error: Revealed type is 'builtins.float'

reveal.py:8: error: Revealed local types are:
reveal.py:8: error: circumference: builtins.float
reveal.py:8: error: radius: builtins.int

即使沒有任何注釋,Mypy也正確地推斷了內置數學的類型。以及我們的局部變量半徑和周長。

注意:以上代碼需要通過mypy運行,如果用python運行會報錯,另外mypy 版本不低於 0.610

Variable Annotations[變量注解]

有時類型檢查器也需要幫助來確定變量的類型。變量注釋在PEP 526中定義,並在Python 3.6中引入。語法與函數參數注釋相同:

pi: float = 3.142

def circumference(radius: float) -> float:
    return 2 * pi * radius

pi被聲明為float類型。

注意: 靜態類型檢查器能夠很好地確定3.142是一個浮點數,因此在本例中不需要pi的注釋。隨着您對Python類型系統的了解越來越多,您將看到更多有關變量注釋的示例。.
變量注釋存儲在模塊級__annotations__字典中::

>>> circumference(1)
6.284

>>> __annotations__
{'pi': <class 'float'>}

即使只是定義變量沒有給賦值,也可以通過__annotations__獲取其類型。雖然在python中沒有賦值的變量直接輸出是錯誤的。

>>> nothing: str
>>> nothing
NameError: name 'nothing' is not defined

>>> __annotations__
{'nothing': <class 'str'>}

Type Comments[類型注解]

如上所述,注釋是在Python 3中引入的,並且它們沒有被反向移植到Python 2.這意味着如果您正在編寫需要支持舊版Python的代碼,則無法使用注釋。

要向函數添加類型注釋,您可以執行以下操作:

import math

def circumference(radius):
    # type: (float) -> float
    return 2 * math.pi * radius

類型注釋只是注釋,所以它們可以用在任何版本的Python中。

類型注釋由類型檢查器直接處理,所以不存在__annotations__字典對象中:

>>> circumference.__annotations__
{}

類型注釋必須以type: 字面量開頭,並與函數定義位於同一行或下一行。如果您想用幾個參數來注釋一個函數,您可以用逗號分隔每個類型:

def headline(text, width=80, fill_char="-"):
    # type: (str, int, str) -> str
    return f" {text.title()} ".center(width, fill_char)

print(headline("type comments work", width=40))

您還可以使用自己的注釋在單獨的行上編寫每個參數:

# headlines.py
 
  def headline(
      text,           # type: str
      width=80,       # type: int
      fill_char="-",  # type: str
  ):                  # type: (...) -> str
      return f" {text.title()} ".center(width, fill_char)
 
 print(headline("type comments work", width=40))

通過Python和Mypy運行示例:

$  python headlines.py
---------- Type Comments Work ----------

$ mypy headline.py
$

如果傳入一個字符串width="full",再次運行mypy會出現一下錯誤。

$ mypy headline.py
headline.py:10: error: Argument "width" to "headline" has incompatible
                       type "str"; expected "int"

您還可以向變量添加類型注釋。這與您向參數添加類型注釋的方式類似:

pi = 3.142  # type: float

上面的例子可以檢測出pi是float類型。

So, Type Annotations or Type Comments?

所以向自己的代碼添加類型提示時,應該使用注釋還是類型注釋?簡而言之:盡可能使用注釋,必要時使用類型注釋。
注釋提供了更清晰的語法,使類型信息更接近您的代碼。它們也是官方推薦的寫入類型提示的方式,並將在未來進一步開發和適當維護。

類型注釋更詳細,可能與代碼中的其他類型注釋沖突,如linter指令。但是,它們可以用在不支持注釋的代碼庫中。

還有一個隱藏選項3:存根文件。稍后,當我們討論向第三方庫添加類型時,您將了解這些。

存根文件可以在任何版本的Python中使用,代價是必須維護第二組文件。通常,如果無法更改原始源代碼,則只需使用存根文件。

玩轉Type Hint, Part 1

到目前為止,您只在類型提示中使用了str,float和bool等基本類型。但是Python類型系統非常強大,它可以支持多種更復雜的類型。
在本節中,您將了解有關此類型系統的更多信息,同時實現簡單的紙牌游戲。您將看到如何指定:

序列和映射的類型,如元組,列表和字典
鍵入別名,使代碼更容易閱讀
該函數和方法不返回任何內容
可以是任何類型的對象
在簡要介紹了一些類型理論之后,您將看到更多用Python指定類型的方法。您可以在這里找到代碼示例:

https://github.com/realpython/materials/tree/master/python-type-checking

Example: A Deck of Cards

以下示例顯示了一副常規紙牌的實現:

  # game.py
 
  import random
 
  SUITS = "♠ ♡ ♢ ♣".split()
  RANKS = "2 3 4 5 6 7 8 9 10 J Q K A".split()
 
  def create_deck(shuffle=False):
      """Create a new deck of 52 cards"""
     deck = [(s, r) for r in RANKS for s in SUITS]
     if shuffle:
         random.shuffle(deck)
     return deck

 def deal_hands(deck):
     """Deal the cards in the deck into four hands"""
     return (deck[0::4], deck[1::4], deck[2::4], deck[3::4])

 def play():
     """Play a 4-player card game"""
     deck = create_deck(shuffle=True)
     names = "P1 P2 P3 P4".split()
     hands = {n: h for n, h in zip(names, deal_hands(deck))}
     for name, cards in hands.items():
         card_str = " ".join(f"{s}{r}" for (s, r) in cards)
         print(f"{name}: {card_str}")

 if __name__ == "__main__":
     play()

每張卡片都表示為套裝和等級的字符串元組。卡組表示為卡片列表。create_deck()創建一個由52張撲克牌組成的常規套牌,並可選擇隨機播放這些牌。deal_hands()將牌組交給四名玩家。

最后,play()扮演游戲。截至目前,它只是通過構建一個洗牌套牌並向每個玩家發牌來准備紙牌游戲。以下是典型輸出:

$ python game.py
P4: ♣9 ♢9 ♡2 ♢7 ♡7 ♣A ♠6 ♡K ♡5 ♢6 ♢3 ♣3 ♣Q
P1: ♡A ♠2 ♠10 ♢J ♣10 ♣4 ♠5 ♡Q ♢5 ♣6 ♠A ♣5 ♢4
P2: ♢2 ♠7 ♡8 ♢K ♠3 ♡3 ♣K ♠J ♢A ♣7 ♡6 ♡10 ♠K
P3: ♣2 ♣8 ♠8 ♣J ♢Q ♡9 ♡J ♠4 ♢8 ♢10 ♠9 ♡4 ♠Q

下面讓我一步一步對上面的代碼進行拓展。

Sequences and Mappings[序列和映射]

讓我們為我們的紙牌游戲添加類型提示。換句話說,讓我們注釋函數create_deck(),deal_hands()和play()。第一個挑戰是你需要注釋復合類型,例如用於表示卡片組的列表和用於表示卡片本身的元組。

對於像str、float和bool這樣的簡單類型,添加類型提示就像使用類型本身一樣簡單:

>>> name: str = "Guido"
>>> pi: float = 3.142
>>> centered: bool = False

對於復合類型,可以執行相同的操作:

>>> names: list = ["Guido", "Jukka", "Ivan"]
>>> version: tuple = (3, 7, 1)
>>> options: dict = {"centered": False, "capitalize": True}

上面的注釋還是不完善,比如names我們只是知道這是list類型,但是我們不知道list里面的元素數據類型

typing模塊為我們提供了更精准的定義:

>>> from typing import Dict, List, Tuple

>>> names: List[str] = ["Guido", "Jukka", "Ivan"]
>>> version: Tuple[int, int, int] = (3, 7, 1)
>>> options: Dict[str, bool] = {"centered": False, "capitalize": True}

需要注意的是,這些類型中的每一個都以大寫字母開頭,並且它們都使用方括號來定義項的類型:

  • names 是一個str類型的list數組。
  • version 是一個含有3個int類型的元組
  • options 是一個字典鍵名類型str,簡直類型bool

Typing 還包括其他的很多類型比如 Counter, Deque, FrozenSet, NamedTuple, 和 Set.此外,該模塊還包括其他的類型,你將在后面的部分中看到.

讓我們回到撲克游戲. 因為卡片是有2個str組成的元組定義的. 所以你可以寫作Tuple[str, str],所以函數create_deck()返回值的類型就是 List[Tuple[str, str]].

 def create_deck(shuffle: bool = False) -> List[Tuple[str, str]]:
     """Create a new deck of 52 cards"""
     deck = [(s, r) for r in RANKS for s in SUITS]
     if shuffle:
        random.shuffle(deck)
     return deck

除了返回值之外,您還將bool類型添加到可選的shuffle參數中。

注意: 元組和列表的聲明是有區別的
元組是不可變序列,通常由固定數量的可能不同類型的元素組成。例如,我們將卡片表示為套裝和等級的元組。通常,您為n元組編寫Tuple[t_1,t_2,...,t_n]

列表是可變序列,通常由未知數量的相同類型的元素組成,例如卡片列表。無論列表中有多少元素,注釋中只有一種類型:List[t]

在許多情況下,你的函數會期望某種序列,並不關心它是列表還是元組。在這些情況下,您應該使用typing.Sequence在注釋函數參數時:

from typing import List, Sequence

def square(elems: Sequence[float]) -> List[float]:
    return [x**2 for x in elems]

使用 Sequence 是一個典型的鴨子類型的例子. 也就意味着可以使用len() 和 .
_getitem_()等方法。

Type Aliases[類型別名]

使用嵌套類型(如卡片組)時,類型提示可能會變得非常麻煩。你可能需要仔細看List [Tuple [str,str]],才能確定它與我們的一副牌是否相符.

現在考慮如何注釋deal_hands()函數:

def deal_hands(deck: List[Tuple[str, str]]) -> Tuple[
     List[Tuple[str, str]],
     List[Tuple[str, str]],
     List[Tuple[str, str]],
     List[Tuple[str, str]],
 ]:
     """Deal the cards in the deck into four hands"""
     return (deck[0::4], deck[1::4], deck[2::4], deck[3::4])

這也太麻煩了!

不怕,我們還可以使用起別名的方式把注解的類型賦值給一個新的變量,方便在后面使用,就像下面這樣:

from typing import List, Tuple

Card = Tuple[str, str]
Deck = List[Card]

現在我們就可以使用別名對之前的代碼進行注解了:

def deal_hands(deck: Deck) -> Tuple[Deck, Deck, Deck, Deck]:
     """Deal the cards in the deck into four hands"""
     return (deck[0::4], deck[1::4], deck[2::4], deck[3::4])

類型別名讓我們的代碼變的簡潔了不少,我們可以打印變量看里面具體的值:

>>> from typing import List, Tuple
>>> Card = Tuple[str, str]
>>> Deck = List[Card]

>>> Deck
typing.List[typing.Tuple[str, str]]

當輸出Deck的時候可以看到其最終的類型.

函數無返回值時

對於沒有返回值的函數,我們可以指定None:

 # play.py
 
  def play(player_name: str) -> None:
      print(f"{player_name} plays")
 
  ret_val = play("Filip")

通過mypy檢測上面代碼

$ mypy play.py
play.py:6: error: "play" does not return a value

作為一個更奇特的情況,請注意您還可以注釋從未期望正常返回的函數。這是使用NoReturn完成的:

from typing import NoReturn

def black_hole() -> NoReturn:
    raise Exception("There is no going back ...")

因為black_hole()總是引發異常,所以它永遠不會正確返回。

Example: Play Some Cards

讓我們回到我們的紙牌游戲示例。在游戲的第二個版本中,我們像以前一樣向每個玩家發放一張牌。然后選擇一個開始玩家並且玩家輪流玩他們的牌。雖然游戲中沒有任何規則,所以玩家只會玩隨機牌:

  # game.py
 
  import random
  from typing import List, Tuple
 
  SUITS = "♠ ♡ ♢ ♣".split()
  RANKS = "2 3 4 5 6 7 8 9 10 J Q K A".split()
 
  Card = Tuple[str, str]
  Deck = List[Card]
 
 def create_deck(shuffle: bool = False) -> Deck:
     """Create a new deck of 52 cards"""
     deck = [(s, r) for r in RANKS for s in SUITS]
     if shuffle:
         random.shuffle(deck)
     return deck

 def deal_hands(deck: Deck) -> Tuple[Deck, Deck, Deck, Deck]:
     """Deal the cards in the deck into four hands"""
     return (deck[0::4], deck[1::4], deck[2::4], deck[3::4])

 def choose(items):
     """Choose and return a random item"""
     return random.choice(items)

 def player_order(names, start=None):
     """Rotate player order so that start goes first"""
     if start is None:
         start = choose(names)
     start_idx = names.index(start)
     return names[start_idx:] + names[:start_idx]

 def play() -> None:
     """Play a 4-player card game"""
     deck = create_deck(shuffle=True)
     names = "P1 P2 P3 P4".split()
     hands = {n: h for n, h in zip(names, deal_hands(deck))}
     start_player = choose(names)
     turn_order = player_order(names, start=start_player)

     # Randomly play cards from each player's hand until empty
     while hands[start_player]:
         for name in turn_order:
             card = choose(hands[name])
             hands[name].remove(card)
             print(f"{name}: {card[0] + card[1]:<3}  ", end="")
         print()

 if __name__ == "__main__":
     play()

請注意,除了更改play()之外,我們還添加了兩個需要類型提示的新函數:choose()和player_order()。在討論我們如何向它們添加類型提示之前,以下是運行游戲的示例輸出:

$ python game.py
P3: ♢10  P4: ♣4   P1: ♡8   P2: ♡Q
P3: ♣8   P4: ♠6   P1: ♠5   P2: ♡K
P3: ♢9   P4: ♡J   P1: ♣A   P2: ♡A
P3: ♠Q   P4: ♠3   P1: ♠7   P2: ♠A
P3: ♡4   P4: ♡6   P1: ♣2   P2: ♠K
P3: ♣K   P4: ♣7   P1: ♡7   P2: ♠2
P3: ♣10  P4: ♠4   P1: ♢5   P2: ♡3
P3: ♣Q   P4: ♢K   P1: ♣J   P2: ♡9
P3: ♢2   P4: ♢4   P1: ♠9   P2: ♠10
P3: ♢A   P4: ♡5   P1: ♠J   P2: ♢Q
P3: ♠8   P4: ♢7   P1: ♢3   P2: ♢J
P3: ♣3   P4: ♡10  P1: ♣9   P2: ♡2
P3: ♢6   P4: ♣6   P1: ♣5   P2: ♢8

在該示例中,隨機選擇玩家P3作為起始玩家。反過來,每個玩家都會玩一張牌:先是P3,然后是P4,然后是P1,最后是P2。只要手中有任何左手,玩家就會持續打牌。

The Any Type

choose()適用於名稱列表和卡片列表(以及任何其他序列)。為此添加類型提示的一種方法是:

import random
from typing import Any, Sequence

def choose(items: Sequence[Any]) -> Any:
    return random.choice(items)

這或多或少意味着它:items是一個可以包含任何類型的項目的序列,而choose()將返回任何類型的這樣的項目。不是很嚴謹,此時請考慮以下示例:

  # choose.py
 
  import random
  from typing import Any, Sequence
 
  def choose(items: Sequence[Any]) -> Any:
      return random.choice(items)
 
  names = ["Guido", "Jukka", "Ivan"]
  reveal_type(names)

  name = choose(names)
  reveal_type(name)

雖然Mypy會正確推斷名稱是字符串列表,但由於使用了任意類型,在調用choose ( )后,該信息會丟失:

$ mypy choose.py
choose.py:10: error: Revealed type is 'builtins.list[builtins.str*]'
choose.py:13: error: Revealed type is 'Any'

由此可以得知,如果使用了Any使用mypy的時候將不容易檢測。

玩轉 Type Hint, Part 2

import random
from typing import Any, Sequence

def choose(items: Sequence[Any]) -> Any:
    return random.choice(items)

使用Any的問題在於您不必要地丟失類型信息。您知道如果將一個字符串列表傳遞給choose(),它將返回一個字符串。

Type Variables[類型聲明]

類型聲明是一個特殊變量聲明,可以采用任何類型,具體取決於具體情況。
讓我們創建一個有效封裝choose()行為的類型變量:

  # choose.py
 
  import random
  from typing import Sequence, TypeVar
 
  Choosable = TypeVar("Chooseable")
 
  def choose(items: Sequence[Choosable]) -> Choosable:
      return random.choice(items)

  names = ["Guido", "Jukka", "Ivan"]
  reveal_type(names)

  name = choose(names)
  reveal_type(name)

類型聲明必須使用類型模塊中的 TypeVar 定義。使用時,類型聲明的范圍覆蓋所有可能的類型,並獲取最特定的類型。在這個例子中,name現在是一個str

$ mypy choose.py
choose.py:12: error: Revealed type is 'builtins.list[builtins.str*]'
choose.py:15: error: Revealed type is 'builtins.str*'

考慮一些其他例子:

  # choose_examples.py
 
  from choose import choose
 
  reveal_type(choose(["Guido", "Jukka", "Ivan"]))
  reveal_type(choose([1, 2, 3]))
  reveal_type(choose([True, 42, 3.14]))
  reveal_type(choose(["Python", 3, 7])

前兩個例子應該有類型str和int,但是后兩個呢?單個列表項有不同的類型,在這種情況下,可選擇類型變量會盡最大努力適應:

$ mypy choose_examples.py
choose_examples.py:5: error: Revealed type is 'builtins.str*'
choose_examples.py:6: error: Revealed type is 'builtins.int*'
choose_examples.py:7: error: Revealed type is 'builtins.float*'
choose_examples.py:8: error: Revealed type is 'builtins.object*'

正如您已經看到的那樣bool是int的子類型,它也是float的子類型。所以在第三個例子中,choose()的返回值保證可以被認為是浮點數。在最后一個例子中,str和int之間沒有子類型關系,因此關於返回值可以說最好的是它是一個對象。

請注意,這些示例都沒有引發類型錯誤。有沒有辦法告訴類型檢查器,選擇( )應該同時接受字符串和數字,但不能同時接受兩者?

您可以通過列出可接受的類型來約束類型變量:

 # choose.py
 
  import random
  from typing import Sequence, TypeVar
 
  Choosable = TypeVar("Choosable", str, float)
 
  def choose(items: Sequence[Choosable]) -> Choosable:
      return random.choice(items)

  reveal_type(choose(["Guido", "Jukka", "Ivan"]))
  reveal_type(choose([1, 2, 3]))
  reveal_type(choose([True, 42, 3.14]))
  reveal_type(choose(["Python", 3, 7]))

現在Choosable只能是str或float,而Mypy會注意到最后一個例子是一個錯誤:

$ mypy choose.py
choose.py:11: error: Revealed type is 'builtins.str*'
choose.py:12: error: Revealed type is 'builtins.float*'
choose.py:13: error: Revealed type is 'builtins.float*'
choose.py:14: error: Revealed type is 'builtins.object*'
choose.py:14: error: Value of type variable "Choosable" of "choose"
                     cannot be "object"

還要注意,在第二個例子中,即使輸入列表只包含int對象,該類型也被認為是float類型的。這是因為Choosable僅限於str和float,int是float的一個子類型。

在我們的紙牌游戲中,我們想限制choose()只能用str和Card類型:

Choosable = TypeVar("Choosable", str, Card)

def choose(items: Sequence[Choosable]) -> Choosable:
    ...

我們簡要地提到Sequence表示列表和元組。正如我們所指出的,一個Sequence可以被認為是一個duck類型,因為它可以是實現了.__ len _()和._ getitem\ __()的任何對象。

鴨子類型和協議

回想一下引言中的以下例子:

def len(obj):
    return obj.__len__()

len()方法可以返回任何實現__len__魔法函數的對象的長度,那我們如何在len()里添加類型提示,尤其是參數obj的類型表示呢?

答案隱藏在學術術語structural subtyping[https://en.wikipedia.org/wiki/Structural_type_system]。structural subtyping的一種方法是根據它們是normal的還是structural的:
在normal系統中,類型之間的比較基於名稱和聲明。Python類型系統大多是名義上的,因為它們的子類型關系,可以用int來代替float。

在structural系統中,類型之間的比較基於結構。您可以定義一個結構類型“大小”,它包括定義的所有實例。_len_(),無論其標稱類型如何.

目前正在通過PEP 544為Python帶來一個成熟的結構類型系統,該系統旨在添加一個稱為協議的概念。盡管大多數PEP 544已經在Mypy中實現了。

協議指定了一個或多個實現的方法。例如,所有類定義。_len_()完成typing.Sized協議。因此,我們可以將len()注釋如下:

from typing import Sized

def len(obj: Sized) -> int:
    return obj.__len__()

除此之外,在Typing中還包括以下模塊 Container, Iterable, Awaitable, 還有 ContextManager.

你也可以聲明自定的協議,通過導入typing_extensions模塊中的Protocol協議對象,然后寫一個繼承該方法的子類,像下面這樣:

from typing_extensions import Protocol

class Sized(Protocol):
    def __len__(self) -> int: ...

def len(obj: Sized) -> int:
    return obj.__len__()

需要通過pip安裝上面使用的第三方模塊

 pip install typing-extensions.

Optional 類型

在python中有一種公共模式,就是設置參數的默認值None,這樣做通常是為了避免可變默認值的問題,或者讓一個標記值標記特殊行為。

在上面的card 例子中, 函數 player_order() 使用 None 作為參數start的默認值,表示還沒有指定玩家:

 def player_order(names, start=None):
     """Rotate player order so that start goes first"""
     if start is None:
         start = choose(names)
     start_idx = names.index(start)
     return names[start_idx:] + names[:start_idx]

這給類型提示帶來的挑戰是,通常start應該是一個字符串。但是,它也可能采用特殊的非字符串值“None”。

為解決上面的問題,這里可以使用Optional類型:

from typing import Sequence, Optional

def player_order(
    names: Sequence[str], start: Optional[str] = None
) -> Sequence[str]:
    ...

等價於Union類型的 Union[None, str],意思是這個參數的值類型為str,默認的話可以是

請注意,使用Optional或Union時,必須注意變量是否在后面有操作。比如上面的例子通過判斷start是否為None。如果不判斷None的情況,在做靜態類型檢查的時候會發生錯誤:

 1 # player_order.py
 2
 3 from typing import Sequence, Optional
 4
 5 def player_order(
 6     names: Sequence[str], start: Optional[str] = None
 7 ) -> Sequence[str]:
 8     start_idx = names.index(start)
 9     return names[start_idx:] + names[:start_idx]

Mypy告訴你還沒有處理start為None的情況。

$ mypy player_order.py
player_order.py:8: error: Argument 1 to "index" of "list" has incompatible
                          type "Optional[str]"; expected "str"

也可以使用以下操作,聲明參數start的類型。

def player_order(names: Sequence[str], start: str = None) -> Sequence[str]:
    ...

如果你不想 Mypy 出現報錯,你可以使用命令

 --no-implicit-optional 
Example: The Object(ive) of the Game

接下來我們會重寫上面的撲克牌游戲,讓它看起來更面向對象,以及適當的使用注解。

將我們的紙牌游戲翻譯成以下幾個類, Card, Deck, Player, Game ,下面是代碼實現。

# game.py
 
 import random
 import sys
 
  class Card:
     SUITS = "♠ ♡ ♢ ♣".split()
     RANKS = "2 3 4 5 6 7 8 9 10 J Q K A".split()
 
     def __init__(self, suit, rank):
         self.suit = suit
        self.rank = rank
 
     def __repr__(self):
         return f"{self.suit}{self.rank}"
 
 class Deck:
     def __init__(self, cards):
         self.cards = cards
 
     @classmethod
     def create(cls, shuffle=False):
         """Create a new deck of 52 cards"""
         cards = [Card(s, r) for r in Card.RANKS for s in Card.SUITS]
         if shuffle:
             random.shuffle(cards)
         return cls(cards)
 
     def deal(self, num_hands):
         """Deal the cards in the deck into a number of hands"""
         cls = self.__class__
         return tuple(cls(self.cards[i::num_hands]) for i in range(num_hands))
 
 class Player:
     def __init__(self, name, hand):
         self.name = name
         self.hand = hand
 
     def play_card(self):
         """Play a card from the player's hand"""
         card = random.choice(self.hand.cards)
         self.hand.cards.remove(card)
         print(f"{self.name}: {card!r:<3}  ", end="")
         return card
 
 class Game:
     def __init__(self, *names):
         """Set up the deck and deal cards to 4 players"""
         deck = Deck.create(shuffle=True)
         self.names = (list(names) + "P1 P2 P3 P4".split())[:4]
         self.hands = {
             n: Player(n, h) for n, h in zip(self.names, deck.deal(4))
         }
 
     def play(self):
         """Play a card game"""
         start_player = random.choice(self.names)
         turn_order = self.player_order(start=start_player)
 
         # Play cards from each player's hand until empty
         while self.hands[start_player].hand.cards:
             for name in turn_order:
                 self.hands[name].play_card()
             print()
 
     def player_order(self, start=None):
         """Rotate player order so that start goes first"""
         if start is None:
             start = random.choice(self.names)
         start_idx = self.names.index(start)
         return self.names[start_idx:] + self.names[:start_idx]
 
 if __name__ == "__main__":
     # Read player names from command line
     player_names = sys.argv[1:]
     game = Game(*player_names)
     game.play()

好了,下面讓我們添加注解

Type Hints for Methods

方法的類型提示與函數的類型提示非常相似。唯一的區別是self參數不需要注釋,因為它是一個類的實例。Card類的類型很容易添加:

  class Card:
     SUITS = "♠ ♡ ♢ ♣".split()
     RANKS = "2 3 4 5 6 7 8 9 10 J Q K A".split()
 
     def __init__(self, suit: str, rank: str) -> None:
         self.suit = suit
         self.rank = rank
 
     def __repr__(self) -> str:
         return f"{self.suit}{self.rank}"

注意:_init_() 的返回值總是為None

Class作為類型

類別和類型之間有對應關系。例如,Card的所有實例一起形成Card類型。要使用類作為類型,只需使用類的名稱Card。
例如:Deck(牌組)本質上由一組Card對象組成,你可以像下面這樣去聲明

 class Deck:
    def __init__(self, cards: List[Card]) -> None:
         self.cards = cards

但是,當您需要引用當前定義的類時,這種方法就不那么有效了。例如,Deck.create() 類方法返回一個帶有Deck類型的對象。但是,您不能簡單地添加-> Deck,因為Deck類還沒有完全定義。

這種情況下可以在注釋中使用字符串文字。就像下面使用"Deck",聲明了返回類型,然后加入docstring注釋進一步說明方法。

class Deck:
    @classmethod
    def create(cls, shuffle: bool = False) -> "Deck":
         """Create a new deck of 52 cards"""
         cards = [Card(s, r) for r in Card.RANKS for s in Card.SUITS]
         if shuffle:
             random.shuffle(cards)
         return cls(cards)

Player類也可以直接使用 Deck作為類型聲明. 因為在前面我們已經定義它

 class Player:
     def __init__(self, name: str, hand: Deck) -> None:
         self.name = name
         self.hand = hand

通常,注釋不會在運行時使用。這為推遲對注釋的評估提供了動力。該提議不是將注釋評估為Python表達式並存儲其值,而是存儲注釋的字符串表示形式,並僅在需要時對其進行評估。

這種功能計划在Python 4.0中成為標准。但是,在Python 3.7及更高版本中,可以通過導入__future__屬性的annotations來實現:

from __future__ import annotations

class Deck:
    @classmethod
    def create(cls, shuffle: bool = False) -> Deck:
        ...

使用 __future__之后就可以使用Deck對象替換字符串"Deck"了。

返回 self 或者 cls

如前所述,通常不應該注釋self或cls參數。在一定程度上,這是不必要的,因為self指向類的實例,所以它將具有類的類型。在Card示例中,self擁有隱式類型Card。此外,顯式地添加這種類型會很麻煩,因為還沒有定義該類。所以需要使用字符串“Card”聲明返回類型。
但是,有一種情況可能需要注釋self或cls。考慮如果你有一個其他類繼承的超類,並且有返回self或cls的方法會發生什么:

 # dogs.py
 
 from datetime import date
 
 class Animal:
     def __init__(self, name: str, birthday: date) -> None:
         self.name = name
         self.birthday = birthday
 
     @classmethod
     def newborn(cls, name: str) -> "Animal":
         return cls(name, date.today())
 
     def twin(self, name: str) -> "Animal":
         cls = self.__class__
         return cls(name, self.birthday)
 
 class Dog(Animal):
     def bark(self) -> None:
         print(f"{self.name} says woof!")
 
 fido = Dog.newborn("Fido")
 pluto = fido.twin("Pluto")
 fido.bark()
 pluto.bark()

運行上面的代碼,Mypy會拋出下面的錯誤:

$ mypy dogs.py
dogs.py:24: error: "Animal" has no attribute "bark"
dogs.py:25: error: "Animal" has no attribute "bark"

問題是,即使繼承的Dog.newborn()和Dog.twin()方法將返回一個Dog,注釋表明它們返回一個Animal。

在這種情況下,您需要更加小心以確保注釋正確。返回類型應與self的類型或cls的實例類型匹配。這可以使用TypeVar來完成,這些變量會跟蹤實際傳遞給self和cls的內容:

# dogs.py

from datetime import date
from typing import Type, TypeVar

TAnimal = TypeVar("TAnimal", bound="Animal")

class Animal:
    def __init__(self, name: str, birthday: date) -> None:
        self.name = name
        self.birthday = birthday

    @classmethod
    def newborn(cls: Type[TAnimal], name: str) -> TAnimal:
        return cls(name, date.today())

    def twin(self: TAnimal, name: str) -> TAnimal:
        cls = self.__class__
        return cls(name, self.birthday)

class Dog(Animal):
    def bark(self) -> None:
        print(f"{self.name} says woof!")

fido = Dog.newborn("Fido")
pluto = fido.twin("Pluto")
fido.bark()
pluto.bark()

在這個例子中有幾個需要注意的點:

類型變量TAnimal用於表示返回值可能是Animal的子類的實例。.

我們指定Animal是TAnimal的上限。指定綁定意味着TAnimal將是Animal子類之一。這可以正確限制所允許的類型。

typing.Type []是type()的類型。需要注意,是cls的類方法需要使用這種形式注解,而self就不用使用。

注解 *args 和 **kwargs

在面向對象的游戲版本中,我們添加了在命令行上命名玩家的選項。這是通過在程序名稱后面列出玩家名稱來完成的:

$ python game.py GeirArne Dan Joanna
Dan: ♢A   Joanna: ♡9   P1: ♣A   GeirArne: ♣2
Dan: ♡A   Joanna: ♡6   P1: ♠4   GeirArne: ♢8
Dan: ♢K   Joanna: ♢Q   P1: ♣K   GeirArne: ♠5
Dan: ♡2   Joanna: ♡J   P1: ♠7   GeirArne: ♡K
Dan: ♢10  Joanna: ♣3   P1: ♢4   GeirArne: ♠8
Dan: ♣6   Joanna: ♡Q   P1: ♣Q   GeirArne: ♢J
Dan: ♢2   Joanna: ♡4   P1: ♣8   GeirArne: ♡7
Dan: ♡10  Joanna: ♢3   P1: ♡3   GeirArne: ♠2
Dan: ♠K   Joanna: ♣5   P1: ♣7   GeirArne: ♠J
Dan: ♠6   Joanna: ♢9   P1: ♣J   GeirArne: ♣10
Dan: ♠3   Joanna: ♡5   P1: ♣9   GeirArne: ♠Q
Dan: ♠A   Joanna: ♠9   P1: ♠10  GeirArne: ♡8
Dan: ♢6   Joanna: ♢5   P1: ♢7   GeirArne: ♣4

關於類型注釋:即使名稱是字符串元組,也應該只注釋每個名稱的類型。換句話說,您應該使用字符串而不是元組[字符串],就像下面這個例子:

class Game:
     def __init__(self, *names: str) -> None:
         """Set up the deck and deal cards to 4 players"""
         deck = Deck.create(shuffle=True)
         self.names = (list(names) + "P1 P2 P3 P4".split())[:4]
         self.hands = {
             n: Player(n, h) for n, h in zip(self.names, deck.deal(4))
         }

類似地,如果有一個接受**kwargs的函數或方法,那么你應該只注釋每個可能的關鍵字參數的類型。

Callables可調用類型

函數是Python中的一類對象。可以使用函數作為其他函數的參數。這意味着需要能夠添加表示函數的類型提示。
函數以及lambdas、方法和類都由type的Callable對象表示。參數的類型和返回值通常也表示。例如,Callable[[A1, A2, A3],Rt]表示一個函數,它有三個參數,分別具有A1、A2和A3類型。函數的返回類型是Rt。

在下面這個例子, 函數 do_twice() 傳入一個Callable類型的func參數,並指明傳入的函數的參數類型為str,返回值類型為str。比如傳入參數create_greeting.

  # do_twice.py
  
  from typing import Callable
  
  def do_twice(func: Callable[[str], str], argument: str) -> None:
      print(func(argument))
      print(func(argument))
 
  def create_greeting(name: str) -> str:
     return f"Hello {name}"
 
  do_twice(create_greeting, "Jekyll")

Example: Hearts

讓我們以紅心游戲的完整例子來結束。您可能已經從其他計算機模擬中了解了這個游戲。下面是對規則的簡要回顧:
四名玩家每人玩13張牌。
持有♣2的玩家開始第一輪,必須出♣2。
如果可能的話,玩家輪流打牌,跟隨領頭的一套牌。
在第一套牌中打出最高牌的玩家贏了這個把戲,並在下一個回合中成為開始牌的玩家。
玩家不能用♡,除非♡已經在之前的技巧中玩過。
玩完所有牌后,玩家如果拿到某些牌就會獲得積分:
♠Q為13分
每個♡1為分
一場比賽持續幾輪,直到得到100分以上。得分最少的玩家獲勝

具體游戲規則可以網上搜索一下.

在這個示例中,沒有多少新的類型概念是尚未見過的。因此,我們將不詳細討論這段代碼,而是將其作為帶注釋代碼的示例。

# 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

    @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: 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)
  GeirArne, choose card: 0
GeirArne => ♠4
Aldren -> ♠5
Joanna -> ♠3
Brad -> ♠2
Aldren wins the trick

...

Joanna -> ♡J
Brad -> ♡2
  0: ♡6  1: ♡9  (Rest: )
  GeirArne, choose card: 1
GeirArne => ♡9
Aldren -> ♡A
Aldren wins the trick

Aldren -> ♣A
Joanna -> ♡Q
Brad -> ♣J
  0: ♡6  (Rest: )
  GeirArne, choose card: 0
GeirArne => ♡6
Aldren wins the trick

Scores:
Brad             14  14
Aldren           10  10
GeirArne          1   1
Joanna            1   1

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM