正文共: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具備動態類型:
...3
TypeError: unsupported operand type(s) for +: 'int' and 'str'
在上面例子中,if從未運行過,所以它未被類型檢查過。else部分,當計算1 +「2」時,由於類型不一致因此,會產生一個類型錯誤。
若是改變一個變量的值的類型
<class 'str'>
<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__() 方法:
... def __len__(self):... return 95022...95022
實際len()方法就是下面的這種方法實現的:
def len(obj): return obj.__len__()
由此發現,對象也能夠像str,list,dict那樣使用len方法,只不過須要從新寫__len__魔法函數便可。
Hello Types
def headline(text, align=True): if align: return f"{text.title()}\n{'-' * len(text)}" else: return f" {text.title()} ".center(50, "o")
默認狀況下,函數返回與下劃線對齊的左側標題。經過將align標誌設置爲False,您還能夠選擇使用o圍繞字符串:
Python Type Checking--------------------
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 Type Checking--------------------oooooooooooooooooooo Use Mypy oooooooooooooooooooo
第一個標題與左側對齊,而第二個標題居中。
Pros and Cons
類型提示可幫助您構建和維護更清晰的體系結構。編寫類型提示的行爲迫使您考慮程序中的類型。雖然Python的動態特性是其重要資產之一,可是有意識地依賴於鴨子類型,重載方法或多種返回類型是一件好事。
須要注意的是,類型提示會在啓動時帶來輕微的損失。若是您須要使用類型模塊,那麼導入時間可能很長,尤爲是在簡短的腳本中。
那麼,您應該在本身的代碼中使用靜態類型檢查嗎?這不是一個全有或全無的問題。幸運的是,Python支持漸進式輸入的概念。這意味着您能夠逐漸在代碼中引入類型。沒有類型提示的代碼將被靜態類型檢查器忽略。所以,您能夠開始向關鍵組件添加類型,只要它能爲您增長價值,就能夠繼續。
關因而否向項目添加類型的一些經驗法則:
若是您剛開始學習Python,能夠安全地等待類型提示,直到您有更多經驗。
類型提示在短暫拋出腳本中增長的價值很小。
在其餘人使用的庫中,尤爲是在PyPI上發佈的庫中,類型提示會增長不少價值。使用庫的其餘代碼須要這些類型提示才能正確地進行類型檢查。
在較大的項目中,類型提示能夠幫助您理解類型是如何在代碼中流動的,強烈建議您這樣作。在與他人合做的項目中更是如此。
Bernat Gabor在他的文章《Python中類型提示的狀態》中建議,只要值得編寫單元測試,就應該使用類型提示。實際上,類型提示在代碼中扮演着相似於測試的角色:它們幫助開發人員編寫更好的代碼。
註解
Python 3.0中引入了註釋,最初沒有任何特定用途。它們只是將任意表達式與函數參數和返回值相關聯的一種方法。
多年之後,PEP 484根據Jukka Lehtosalo博士項目Mypy所作的工做,定義瞭如何向Python代碼添加類型提示。添加類型提示的主要方法是使用註釋。隨着類型檢查變得愈來愈廣泛,這也意味着註釋應該主要保留給類型提示。
接下來的章節將解釋註釋如何在類型提示的上下文中工做。
函數註解
以前咱們也提到過函數的註解例子向下面這樣:
def func(arg: arg_type, optarg: arg_type = default) -> return_type: ...
對於參數,語法是參數:註釋,而返回類型使用- >註釋進行註釋。請注意,註釋必須是有效的Python表達式。
如下簡單示例向計算圓周長的函數添加註釋::
import math
def circumference(radius: float) -> float: return 2 * math.pi * radius
通調用circumference對象的__annotations__魔法函數能夠輸出函數的註解信息。
7.728317927830891
{'radius': <class 'float'>, 'return': <class 'float'>}
有時您可能會對Mypy如何解釋您的類型提示感到困惑。對於這些狀況,有一些特殊的Mypy表達式:reveal type()和reveal local()。您能夠在運行Mypy以前將這些添加到您的代碼中,Mypy將報告它所推斷的類型。例如,將如下代碼保存爲reveal.py。
# reveal.py import math reveal_type(math.pi) 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__字典中::
6.284
{'pi': <class 'float'>}
即便只是定義變量沒有給賦值,也能夠經過__annotations__獲取其類型。雖然
在python中沒有賦值的變量直接輸出是錯誤的。
NameError: name 'nothing' is not defined
{'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
在本節中,您將瞭解有關此類型系統的更多信息,同時實現簡單的紙牌遊戲。您將看到如何指定:
序列和映射的類型,如元組,列表和字典
鍵入別名,使代碼更容易閱讀
該函數和方法不返回任何內容
能夠是任何類型的對象
在簡要介紹了一些類型理論以後,您將看到更多用Python指定類型的方法。您能夠在這裏找到代碼示例:https://github.com/realpython/materials/tree/master/python-type-checking
如下示例顯示了一副常規紙牌的實現:
# game.py import random SUITS = "♠ ♡ ♢ ♣".split() RANKS = "2 3 4 5 6 7 8 9 10 J Q K A".split() def create_deck(shuffle=False): """Create a new deck of 52 cards""" deck = [(s, r) for r in RANKS for s in SUITS] if shuffle: random.shuffle(deck) return deck
def deal_hands(deck): """Deal the cards in the deck into four hands""" return (deck[0::4], deck[1::4], deck[2::4], deck[3::4])
def play(): """Play a 4-player card game""" deck = create_deck(shuffle=True) names = "P1 P2 P3 P4".split() hands = {n: h for n, h in zip(names, deal_hands(deck))} for name, cards in hands.items(): card_str = " ".join(f"{s}{r}" for (s, r) in cards) print(f"{name}: {card_str}")
if __name__ == "__main__": play()
每張卡片都表示爲套裝和等級的字符串元組。卡組表示爲卡片列表。create_deck()建立一個由52張撲克牌組成的常規套牌,並可選擇隨機播放這些牌。deal_hands()將牌組交給四名玩家。
最後,play()扮演遊戲。截至目前,它只是經過構建一個洗牌套牌並向每一個玩家發牌來準備紙牌遊戲。如下是典型輸出:
$ python game.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
下面讓我一步一步對上面的代碼進行拓展。
讓咱們爲咱們的紙牌遊戲添加類型提示。換句話說,讓咱們註釋函數create_deck(),deal_hands()和play()。第一個挑戰是你須要註釋複合類型,例如用於表示卡片組的列表和用於表示卡片自己的元組。
對於像str、float和bool這樣的簡單類型,添加類型提示就像使用類型自己同樣簡單:
對於複合類型,能夠執行相同的操做:
上面的註釋仍是不完善,好比names咱們只是知道這是list類型,可是咱們不知道list裏面的元素數據類型
typing模塊爲咱們提供了更精準的定義:
須要注意的是,這些類型中的每個都以大寫字母開頭,而且它們都使用方括號來定義項的類型:
names
是一個str類型的list數組。
version
是一個含有3個int類型的元組
options
是一個字典鍵名類型str,簡直類型bool
typing
還包括其餘的不少類型好比 Counter
, Dequ
e
, FrozenSet
, NamedTuple
, 和 Set
.此外,該模塊還包括其餘的類型,你將在後面的部分中看到.
讓咱們回到撲克遊戲. 由於卡片是有2個str組成的元組定義的. 因此你能夠寫做Tuple[str, str]
,因此函數create_deck()返回值的類型就是 List[Tuple[str, str]]
.
def create_deck(shuffle: bool = False) -> List[Tuple[str, str]]: """Create a new deck of 52 cards""" deck = [(s, r) for r in RANKS for s in SUITS] if shuffle: random.shuffle(deck) return deck
除了返回值以外,您還將bool類型添加到可選的shuffle參數中。
注意: 元組和列表的聲明是有區別的
元組是不可變序列,一般由固定數量的可能不一樣類型的元素組成。例如,咱們將卡片表示爲套裝和等級的元組。一般,您爲n元組編寫元組[t_1,t_2,...,t_n]。
列表是可變序列,一般由未知數量的相同類型的元素組成,例如卡片列表。不管列表中有多少元素,註釋中只有一種類型:List [t]。
在許多狀況下,你的函數會指望某種順序,並不關心它是列表仍是元組。在這些狀況下,您應該使用typing.Sequence在註釋函數參數時:
from typing import List, Sequence
def square(elems: Sequence[float]) -> List[float]: return [x**2 for x in elems]
使用 Sequence
是一個典型的鴨子類型的例子. 也就意味着可使用
len()
和 .__getitem__()等方法。
使用嵌套類型(如卡片組)時,類型提示可能會變得很是麻煩。你可能須要仔細看List [Tuple [str,str]],才能肯定它與咱們的一副牌是否相符.
如今考慮如何註釋deal_hands()
:
def deal_hands(deck: List[Tuple[str, str]]) -> Tuple[ List[Tuple[str, str]], List[Tuple[str, str]], List[Tuple[str, str]], List[Tuple[str, str]], ]: """Deal the cards in the deck into four hands""" return (deck[0::4], deck[1::4], deck[2::4], deck[3::4])
這也太麻煩了!
不怕,咱們還可使用起別名的方式把註解的類型賦值給一個新的變量,方便在後面使用,就像下面這樣:
from typing import List, Tuple
Card = Tuple[str, str]Deck = List[Card]
如今咱們就可使用別名對以前的代碼進行註解了:
def deal_hands(deck: Deck) -> Tuple[Deck, Deck, Deck, Deck]: """Deal the cards in the deck into four hands""" return (deck[0::4], deck[1::4], deck[2::4], deck[3::4])
類型別名讓咱們的代碼變的簡潔了很多,咱們能夠打印變量看裏面具體的值:
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.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( )老是引起異常,因此它永遠不會正確返回。
讓咱們回到咱們的紙牌遊戲示例。在遊戲的第二個版本中,咱們像之前同樣向每一個玩家發放一張牌。而後選擇一個開始玩家而且玩家輪流玩他們的牌。雖然遊戲中沒有任何規則,因此玩家只會玩隨機牌:
# 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。只要手中有任何左手,玩家就會持續打牌。
Any
Typechoose()適用於名稱列表和卡片列表(以及任何其餘序列)。爲此添加類型提示的一種方法是:
import randomfrom typing import Any, Sequence
def choose(items: Sequence[Any]) -> Any: return random.choice(items)
這或多或少意味着它:items是一個能夠包含任何類型的項目的序列,而choose()將返回任何類型的這樣的項目。不是很嚴謹,此時請考慮如下示例:
# choose.py import random from typing import Any, Sequence def choose(items: Sequence[Any]) -> Any: return random.choice(items) names = ["Guido", "Jukka", "Ivan"] reveal_type(names)
name = choose(names) reveal_type(name)
雖然Mypy會正確推斷名稱是字符串列表,但因爲使用了任意類型,在調用choose ( )後,該信息會丟失:
$ mypy choose.pychoose.py:10: error: Revealed type is 'builtins.list[builtins.str*]'choose.py:13: error: Revealed type is 'Any'
由此能夠得知,若是使用了Any使用mypy的時候將不容易檢測。
Playing With Python Types, Part 2
import randomfrom typing import Any, Sequence
def choose(items: Sequence[Any]) -> Any: return random.choice(items)
使用Any的問題在於您沒必要要地丟失類型信息。您知道若是將一個字符串列表傳遞給choose(),它將返回一個字符串。
類型變量
類型變量是一個特殊變量,能夠採用任何類型,具體取決於具體狀況。
讓咱們建立一個有效封裝choose()行爲的類型變量:
# choose.py import random from typing import Sequence, TypeVar Choosable = TypeVar("Chooseable") def choose(items: Sequence[Choosable]) -> Choosable: return random.choice(items)
names = ["Guido", "Jukka", "Ivan"] reveal_type(names)
name = choose(names) reveal_type(name)
類型變量必須使用類型模塊中的TypeVar定義。使用時,類型變量的範圍覆蓋全部可能的類型,並獲取最特定的類型。在這個例子中,name如今是一個str
$ mypy choose.pychoose.py:12: error: Revealed type is 'builtins.list[builtins.str*]'choose.py:15: error: Revealed type is 'builtins.str*'
考慮一些其餘例子:
# choose_examples.py from choose import choose reveal_type(choose(["Guido", "Jukka", "Ivan"])) reveal_type(choose([1, 2, 3])) reveal_type(choose([True, 42, 3.14])) reveal_type(choose(["Python", 3, 7])
前兩個例子應該有類型str和int,可是後兩個呢?單個列表項有不一樣的類型,在這種狀況下,可選擇類型變量會盡最大努力適應:
$ mypy choose_examples.pychoose_examples.py:5: error: Revealed type is 'builtins.str*'choose_examples.py:6: error: Revealed type is 'builtins.int*'choose_examples.py:7: error: Revealed type is 'builtins.float*'choose_examples.py:8: error: Revealed type is 'builtins.object*'
正如您已經看到的那樣bool是int的子類型,它也是float的子類型。因此在第三個例子中,choose()的返回值保證能夠被認爲是浮點數。在最後一個例子中,str和int之間沒有子類型關係,所以關於返回值能夠說最好的是它是一個對象。
請注意,這些示例都沒有引起類型錯誤。有沒有辦法告訴類型檢查器,選擇( )應該同時接受字符串和數字,但不能同時接受二者?
您能夠經過列出可接受的類型來約束類型變量:
# choose.py import random from typing import Sequence, TypeVar Choosable = TypeVar("Choosable", str, float) def choose(items: Sequence[Choosable]) -> Choosable: return random.choice(items)
reveal_type(choose(["Guido", "Jukka", "Ivan"])) reveal_type(choose([1, 2, 3])) reveal_type(choose([True, 42, 3.14])) reveal_type(choose(["Python", 3, 7]))
如今Choosable只能是str或float,而Mypy會注意到最後一個例子是一個錯誤:
$ mypy choose.pychoose.py:11: error: Revealed type is 'builtins.str*'choose.py:12: error: Revealed type is 'builtins.float*'choose.py:13: error: Revealed type is 'builtins.float*'choose.py:14: error: Revealed type is 'builtins.object*'choose.py:14: error: Value of type variable "Choosable" of "choose" cannot be "object"
還要注意,在第二個例子中,即便輸入列表只包含int對象,該類型也被認爲是float類型的。這是由於Choosable僅限於str和float,int是float的一個子類型。
在咱們的紙牌遊戲中,咱們想限制choose()只能用str和Card類型:
Choosable = TypeVar("Choosable", str, Card)
def choose(items: Sequence[Choosable]) -> Choosable: ...
咱們簡要地提到Sequence表示列表和元組。正如咱們所指出的,一個Sequence能夠被認爲是一個duck類型,由於它能夠是實現了.__ len __()和.__ getitem __()的任何對象。
鴨子類型和協議
def len(obj):
return obj.__len__()
len()方法能夠返回任何實現__len__魔法函數的對象的長度,那咱們如何在len()裏添加類型提示,
尤爲是參數obj的類型表示呢?
答案隱藏在學術術語structural subtyping背後。structural subtyping的一種方法是根據它們是normal的仍是structural的:
在normal系統中,類型之間的比較基於名稱和聲明。Python類型系統大可能是名義上的,由於它們的子類型關係,能夠用int來代替float。
在structural系統中,類型之間的比較基於結構。您能夠定義一個結構類型「大小」,它包括定義的全部實例。__len_ _ _(),不管其標稱類型如何.
目前正在經過PEP 544爲Python帶來一個成熟的結構類型系統,該系統旨在添加一個稱爲協議的概念。儘管大多數PEP 544已經在Mypy中實現了。
協議指定了一個或多個實現的方法。例如,全部類定義。_ _ len _ _ _()完成typing.Sized協議。所以,咱們能夠將len()註釋以下:
from typing import Sized
def len(obj: Sized) -> int: return obj.__len__()
除此以外,在Typing中還包括如下模塊 Container
, Iterable
, Awaitable
, 還有 ContextManager
.
你也能夠聲明自定的協議, 經過導入typing_extensions模塊中的Protocol協議對象,而後寫一個繼承該方法的子類,像下面這樣:
from typing_extensions import Protocol
class Sized(Protocol): def __len__(self) -> int: ...
def len(obj: Sized) -> int: return obj.__len__()
到寫本文爲止,須要經過pip安裝上面使用的第三方模塊
pip install typing-extensions.
Optional 類型
在python中有一種公共模式,就是設置參數的默認值None,這樣作一般是爲了不可變默認值的問題,或者讓一個標記值標記特殊行爲。
在上面 的card 例子中, 函數 player_order()
使用 None
做爲參數start的默認值,表示尚未指定玩家:
def player_order(names, start=None): """Rotate player order so that start goes first""" if start is None: start = choose(names) start_idx = names.index(start) return names[start_idx:] + names[:start_idx]
這給類型提示帶來的挑戰是,一般start應該是一個字符串。可是,它也可能採用特殊的非字符串值「None」。
爲解決上面的問題,這裏可使用Optional類型
:
from typing import Sequence, Optional
def player_order( names: Sequence[str], start: Optional[str] = None) -> Sequence[str]: ...
等價於Union類型的
Union[None, str],意思是這個參數的值類型爲str,默認的話能夠是
請注意,使用Optional或Union時,必須注意變量是否在後面有操做。好比上面的例子經過判斷start是否爲None。若是不判斷None的狀況,在作靜態類型檢查的時候會發生錯誤:
1 # player_order.py
2
3 from typing import Sequence, Optional
4
5 def player_order(
6 names: Sequence[str], start: Optional[str] = None
7 ) -> Sequence[str]:
8 start_idx = names.index(start)
9 return names[start_idx:] + names[:start_idx]
Mypy告訴你尚未處理start爲None的狀況。
$ mypy player_order.pyplayer_order.py:8: error: Argument 1 to "index" of "list" has incompatible type "Optional[str]"; expected "str"
也可使用如下操做,聲明參數start的類型。
def player_order(names: Sequence[str], start: str = None) -> Sequence[str]: ...
若是你不想 Mypy 出現報錯,你可使用命令
--no-implicit-optional
Example: The Object(ive) of the Game
接下來咱們會重寫上面的撲克牌遊戲,讓它看起來更面向對象,以及適當的使用註解。
將咱們的紙牌遊戲翻譯成如下幾個類, Card
, Deck
, Player
, Game
,下面是代碼實現。
# game.py import random import sys class Card: SUITS = "♠ ♡ ♢ ♣".split() RANKS = "2 3 4 5 6 7 8 9 10 J Q K A".split() def __init__(self, suit, rank): self.suit = suit self.rank = rank def __repr__(self): return f"{self.suit}{self.rank}" class Deck: def __init__(self, cards): self.cards = cards @classmethod def create(cls, shuffle=False): """Create a new deck of 52 cards""" cards = [Card(s, r) for r in Card.RANKS for s in Card.SUITS] if shuffle: random.shuffle(cards) return cls(cards) def deal(self, num_hands): """Deal the cards in the deck into a number of hands""" cls = self.__class__ return tuple(cls(self.cards[i::num_hands]) for i in range(num_hands)) class Player: def __init__(self, name, hand): self.name = name self.hand = hand def play_card(self): """Play a card from the player's hand""" card = random.choice(self.hand.cards) self.hand.cards.remove(card) print(f"{self.name}: {card!r:<3} ", end="") return card class Game: def __init__(self, *names): """Set up the deck and deal cards to 4 players""" deck = Deck.create(shuffle=True) self.names = (list(names) + "P1 P2 P3 P4".split())[:4] self.hands = { n: Player(n, h) for n, h in zip(self.names, deck.deal(4)) } def play(self): """Play a card game""" start_player = random.choice(self.names) turn_order = self.player_order(start=start_player) # Play cards from each player's hand until empty while self.hands[start_player].hand.cards: for name in turn_order: self.hands[name].play_card() print() def player_order(self, start=None): """Rotate player order so that start goes first""" if start is None: start = random.choice(self.names) start_idx = self.names.index(start) return self.names[start_idx:] + self.names[:start_idx] if __name__ == "__main__": # Read player names from command line player_names = sys.argv[1:] game = Game(*player_names) game.play()
好了,下面讓咱們添加註解
Type Hints for Methods
class Card: SUITS = "♠ ♡ ♢ ♣".split() RANKS = "2 3 4 5 6 7 8 9 10 J Q K A".split() def __init__(self, suit: str, rank: str) -> None: self.suit = suit self.rank = rank def __repr__(self) -> str: return f"{self.suit}{self.rank}"
__init__()
的返回值老是爲None
Class做爲類型
例如:Deck(牌組)本質上由一組Card對象組成,你能夠像下面這樣去聲明
class Deck: def __init__(self, cards: List[Card]) -> None: self.cards = cards
可是,當您須要引用當前定義的類時,這種方法就不那麼有效了。例如,Deck.create() 類方法返回一個帶有Deck類型的對象。可是,您不能簡單地添加-> Deck,由於Deck類尚未徹底定義。
這種狀況下能夠在註釋中使用字符串文字。就像下面使用"Deck",聲明瞭返回類型,而後加入docstring註釋進一步說明方法。
class Deck: @classmethod def create(cls, shuffle: bool = False) -> "Deck": """Create a new deck of 52 cards""" cards = [Card(s, r) for r in Card.RANKS for s in Card.SUITS] if shuffle: random.shuffle(cards) return cls(cards)
Player
類也能夠直接使用 Deck做爲類型聲明
. 由於在前面咱們已經定義它
class Player: def __init__(self, name: str, hand: Deck) -> None: self.name = name self.hand = hand
一般,註釋不會在運行時使用。這爲推遲對註釋的評估提供了動力。該提議不是將註釋評估爲Python表達式並存儲其值,而是存儲註釋的字符串表示形式,並僅在須要時對其進行評估。
這種功能計劃在Python 4.0中成爲標準。可是,在Python 3.7及更高版本中,能夠經過導入__future__屬性的annotations來實現:
from __future__ import annotations
class Deck: @classmethod def create(cls, shuffle: bool = False) -> Deck: ...
使用 __future__以後就可使用Deck對象替換字符串"Deck"了。
返回 self 或者 cls
可是,有一種狀況可能須要註釋self或cls。考慮若是你有一個其餘類繼承的超類,而且有返回self或cls的方法會發生什麼:
# dogs.py from datetime import date class Animal: def __init__(self, name: str, birthday: date) -> None: self.name = name self.birthday = birthday @classmethod def newborn(cls, name: str) -> "Animal": return cls(name, date.today()) def twin(self, name: str) -> "Animal": cls = self.__class__ return cls(name, self.birthday) class Dog(Animal): def bark(self) -> None: print(f"{self.name} says woof!") fido = Dog.newborn("Fido") pluto = fido.twin("Pluto") fido.bark() pluto.bark()
運行上面的代碼,Mypy會拋出下面的錯誤:
$ mypy dogs.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可調用類型
函數以及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
def value(self) -> int: """The value of a card is rank as a number""" return self.RANKS.index(self.rank)
def points(self) -> int: """Points this card is worth""" if self.suit == "♠" and self.rank == "Q": return 13 if self.suit == "♡": return 1 return 0
def __eq__(self, other: Any) -> Any: return self.suit == other.suit and self.rank == other.rank
def __lt__(self, other: Any) -> Any: return self.value < other.value
def __repr__(self) -> str: return f"{self.suit}{self.rank}"
class Deck(Sequence[Card]): def __init__(self, cards: List[Card]) -> None: self.cards = cards
def create(cls, shuffle: bool = False) -> "Deck": """Create a new deck of 52 cards""" cards = [Card(s, r) for r in Card.RANKS for s in Card.SUITS] if shuffle: random.shuffle(cards) return cls(cards)
def play(self, card: Card) -> None: """Play one card by removing it from the deck""" self.cards.remove(card)
def deal(self, num_hands: int) -> Tuple["Deck", ...]: """Deal the cards in the deck into a number of hands""" return tuple(self[i::num_hands] for i in range(num_hands))
def add_cards(self, cards: List[Card]) -> None: """Add a list of cards to the deck""" self.cards += cards
def __len__(self) -> int: return len(self.cards)
def __getitem__(self, key: int) -> Card: ...
def __getitem__(self, key: slice) -> "Deck": ...
def __getitem__(self, key: Union[int, slice]) -> Union[Card, "Deck"]: if isinstance(key, int): return self.cards[key] elif isinstance(key, slice): cls = self.__class__ return cls(self.cards[key]) else: raise TypeError("Indices must be integers or slices")
def __repr__(self) -> str: return " ".join(repr(c) for c in self.cards)
class Player: def __init__(self, name: str, hand: Optional[Deck] = None) -> None: self.name = name self.hand = Deck([]) if hand is None else hand
def playable_cards(self, played: List[Card], hearts_broken: bool) -> Deck: """List which cards in hand are playable this round""" if Card("♣", "2") in self.hand: return Deck([Card("♣", "2")])
lead = played[0].suit if played else None playable = Deck([c for c in self.hand if c.suit == lead]) or self.hand if lead is None and not hearts_broken: playable = Deck([c for c in playable if c.suit != "♡"]) return playable or Deck(self.hand.cards)
def non_winning_cards(self, played: List[Card], playable: Deck) -> Deck: """List playable cards that are guaranteed to not win the trick""" if not played: return Deck([])
lead = played[0].suit best_card = max(c for c in played if c.suit == lead) return Deck([c for c in playable if c < best_card or c.suit != lead])
def play_card(self, played: List[Card], hearts_broken: bool) -> Card: """Play a card from a cpu player's hand""" playable = self.playable_cards(played, hearts_broken) non_winning = self.non_winning_cards(played, playable)
# Strategy if non_winning: # Highest card not winning the trick, prefer points card = max(non_winning, key=lambda c: (c.points, c.value)) elif len(played) < 3: # Lowest card maybe winning, avoid points card = min(playable, key=lambda c: (c.points, c.value)) else: # Highest card guaranteed winning, avoid points card = max(playable, key=lambda c: (-c.points, c.value)) self.hand.cards.remove(card) print(f"{self.name} -> {card}") return card
def has_card(self, card: Card) -> bool: return card in self.hand
def __repr__(self) -> str: return f"{self.__class__.__name__}({self.name!r}, {self.hand})"
class HumanPlayer(Player): def play_card(self, played: List[Card], hearts_broken: bool) -> Card: """Play a card from a human player's hand""" playable = sorted(self.playable_cards(played, hearts_broken)) p_str = " ".join(f"{n}: {c}" for n, c in enumerate(playable)) np_str = " ".join(repr(c) for c in self.hand if c not in playable) print(f" {p_str} (Rest: {np_str})") while True: try: card_num = int(input(f" {self.name}, choose card: ")) card = playable[card_num] except (ValueError, IndexError): pass else: break self.hand.play(card) print(f"{self.name} => {card}") return card
class HeartsGame: def __init__(self, *names: str) -> None: self.names = (list(names) + "P1 P2 P3 P4".split())[:4] self.players = [Player(n) for n in self.names[1:]] self.players.append(HumanPlayer(self.names[0]))
def play(self) -> None: """Play a game of Hearts until one player go bust""" score = Counter({n: 0 for n in self.names}) while all(s < 100 for s in score.values()): print("\nStarting new round:") round_score = self.play_round() score.update(Counter(round_score)) print("Scores:") for name, total_score in score.most_common(4): print(f"{name:<15} {round_score[name]:>3} {total_score:>3}")
winners = [n for n in self.names if score[n] == min(score.values())] print(f"\n{' and '.join(winners)} won the game")
def play_round(self) -> Dict[str, int]: """Play a round of the Hearts card game""" deck = Deck.create(shuffle=True) for player, hand in zip(self.players, deck.deal(4)): player.hand.add_cards(hand.cards) start_player = next( p for p in self.players if p.has_card(Card("♣", "2")) ) tricks = {p.name: Deck([]) for p in self.players} hearts = False
# Play cards from each player's hand until empty while start_player.hand: played: List[Card] = [] turn_order = self.player_order(start=start_player) for player in turn_order: card = player.play_card(played, hearts_broken=hearts) played.append(card) start_player = self.trick_winner(played, turn_order) tricks[start_player.name].add_cards(played) print(f"{start_player.name} wins the trick\n") hearts = hearts or any(c.suit == "♡" for c in played) return self.count_points(tricks)
def player_order(self, start: Optional[Player] = None) -> List[Player]: """Rotate player order so that start goes first""" if start is None: start = random.choice(self.players) start_idx = self.players.index(start) return self.players[start_idx:] + self.players[:start_idx]
def trick_winner(trick: List[Card], players: List[Player]) -> Player: lead = trick[0].suit valid = [ (c.value, p) for c, p in zip(trick, players) if c.suit == lead ] return max(valid)[1]
def count_points(tricks: Dict[str, Deck]) -> Dict[str, int]: return {n: sum(c.points for c in cards) for n, cards in tricks.items()}
if __name__ == "__main__": # Read player names from the command line player_names = sys.argv[1:] game = HeartsGame(*player_names) game.play()
對於上面的代碼有幾個注意點:
對於難以使用Union或類型變量表達的類型關係好比魔法函數,可使用@overload裝飾器。
子類對應於子類型,所以能夠在任何須要玩家的地方使用HumanPlayer。
當子類從超類從新實現方法時,類型註釋必須匹配。有關示例,請參閱HumanPlayer.play_card()。
開始遊戲時,你控制第一個玩家。輸入數字以選擇要玩的牌。下面是一個遊戲的例子,突出顯示的線條顯示了玩家的選擇:
Starting new round:Brad -> ♣2 0: ♣5 1: ♣Q 2: ♣K (Rest: ♢6 ♡10 ♡6 ♠J ♡3 ♡9 ♢10 ♠7 ♠K ♠4) GeirArne => ♣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 => ♠4Aldren -> ♠5Joanna -> ♠3Brad -> ♠2Aldren wins the trick
...
Joanna -> ♡JBrad -> ♡2 0: ♡6 1: ♡9 (Rest: ) GeirArne => ♡9Aldren -> ♡AAldren wins the trick
Aldren -> ♣AJoanna -> ♡QBrad -> ♣J 0: ♡6 (Rest: ) GeirArne => ♡6Aldren wins the trick
Scores:Brad 14 14Aldren 10 10GeirArne 1 1Joanna 1 1
當前目前全部的typing方法的使用場景就結束了。以爲有用的朋友能夠點個已看,或者轉發到朋友圈分享更更多好友。
猜你喜歡