類型檢查
類型系統
所有編程語言都包含某種類型系統,該系統形式化了它可以處理哪些對象類別以及如何處理這些類別。例如,類型系統可以定義數值類型,以數值類型的對象為例。
動態類型
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'
驗證變量是否可以更改類型:
>>> thing = "Hello"
>>> type(thing)
<class 'str'>
>>> thing = 28.1
>>> type(thing)
<class 'float'>
這些示例確認允許更改類型,并且Python會在類型更改時正確推斷類型
靜態類型
動態類型的反面是靜態類型。靜態類型檢查是在不運行程序的情況下執行的。在大多數靜態類型語言中,例如 C 和 Java,這是在編譯程序時完成的。
使用靜態類型時,通常不允許變量更改類型,盡管可能存在將變量轉換為其他類型的機制。
讓我們看一個來自靜態類型語言的快速示例。請考慮以下 Java 代碼段:
String_ thing;
thing_=_"Hello";
第一行聲明變量名在編譯時綁定到類型。該名稱永遠不能重新綁定到其他類型。在第二行中,分配了一個值。永遠不能為其分配不是對象的值。例如,如果你以后說編譯器會因為不兼容的類型而引發錯誤。
Python將始終保持動態類型語言。但是,PEP 484 引入了類型提示,這使得也可以對 Python 代碼進行靜態類型檢查。
與類型在大多數其它靜態類型語言中的工作方式不同,類型提示本身不會導致Python強制類型。顧名思義,類型提示只是建議類型。
鴨子類型(Duck Typing)
談論Python時經常使用的另一個術語是
鴨子類型。這個綽號來自短語“如果它像鴨子一樣走路,像鴨子一樣嘎嘎叫,那么它一定是鴨子”(或其任何變體)。
鴨子類型是一個與動態類型相關的概念,其中對象的類型或類不如它定義的方法重要。使用鴨子類型,您根本不檢查類型。相反,您需要檢查給定方法或屬性是否存在。
例如,您可以調用定義方法的任何 Python 對象:len()``.__len__()
>>> class TheHobbit:
... def __len__(self):
... return 95022
...
>>> the_hobbit = TheHobbit()
>>> len(the_hobbit)
95022
請注意,對 的調用給出了方法的返回值。實際上,實現本質上等效于以下內容:len()``.__len__()``len()
def len(obj):
return obj.__len__()
為了調用,唯一真正的約束是它必須定義一個方法。否則,對象的類型可以與 、 或 不同。len(obj)``obj``.__len__()``str``list``dict``TheHobbit
在使用結構子類型對 Python 代碼進行靜態類型檢查時,在一定程度上支持鴨子類型。
Hello Types
在本節中,你將了解如何向函數添加類型提示。以下函數通過添加適當的大寫和裝飾行將文本字符串轉換為標題:
def headline(text, align=True):
if align:
return f"{text.title()}\n{'-' * len(text)}"
else:
return f" {text.title()} ".center(50, "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:
...
語法說參數應該是類型 .同樣,可選參數應具有默認值 的類型。
在風格方面,PEP8建議如下:
- 對冒號使用常規規則,即冒號前沒有空格,冒號后有一個空格;
- 將參數批注與默認值結合時,請在符號周圍使用空格;
- 在箭頭周圍使用空格。
添加這樣的類型提示沒有運行時效果:它們只是提示,不會自行強制執行。
要捕獲此類錯誤,您可以使用靜態類型檢查器。也就是說,一種檢查代碼類型的工具,而無需實際運行傳統意義上的代碼。不過,進行類型檢查的最常見工具是Mypy。
Mypy應用實例:
將代碼放在名為test.py中:
# test.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 test.py
headlines.py:10: error: Argument "align" to "headline" has incompatible
type "str"; expected "bool"
根據類型提示,Mypy 能夠告訴我們在第 10 行使用了錯誤的類型。
若要修復代碼中的問題,應更改要傳入的參數的值。
優點和缺點
優點
- 類型提示有助于捕獲某些錯誤;
- 類型提示有助于記錄代碼。傳統上,如果要記錄函數參數的預期類型,則可以使用文檔字符串。這是有效的,但由于文檔字符串沒有標準(盡管 PEP 257,它們不能輕易用于自動檢查。
- 類型提示可幫助您構建和維護更簡潔的體系結構。編寫類型提示的行為迫使您考慮程序中的類型。雖然 Python 的動態特性是其重要資產之一,但有意識地依賴鴨子類型、重載方法或多種返回類型是一件好事。
缺點
<靜態類型檢查>
- 添加類型提示需要開發人員花費時間和精力。盡管花更少的時間調試可能會得到回報,但您將花費更多的時間輸入代碼。
- 類型提示會在啟動時間中引入輕微的損失。如果需要使用
鍵入模塊,導入時間可能會很長,尤其是在短腳本中。
小結
那么,你應該在自己的代碼中使用靜態類型檢查嗎?好吧,這不是一個全有或全無的問題。幸運的是,Python支持漸進類型的概念。這意味著您可以逐步將類型引入代碼中。靜態類型檢查器將忽略沒有類型提示的代碼。因此,您可以開始向關鍵組件添加類型,只要它為您增加價值,就可以繼續添加類型。
查看上面的優缺點列表,您會注意到添加類型對正在運行的程序或程序的用戶沒有影響。類型檢查旨在讓您作為開發人員的生活更好、更方便。
關于是否向項目添加類型的一些經驗法則是:
- 如果你剛剛開始學習Python,你可以放心地等地類型提示,直到你有更多地經驗。
- 類型提示在簡短的一次性腳本中幾乎沒有增加價值。
- 在其它人使用地庫中,尤其是在PyPi上發布地,類型提示增加了更多價值。使用庫地其他需要這些類型提示才能正確地進行類型檢查。
- 在較大的項目中,類型提示可幫助您了解類型在代碼中的流動方式,強烈建議使用。
Bernát Gábor在他的優秀文章《Python 中的類型提示狀態》中建議“每當單元測試值得編寫時,都應該使用類型提示。事實上,類型提示在代碼中扮演著與測試類似的角色:它們可以幫助您作為開發人員編寫更好的代碼。
希望您現在對類型檢查在 Python 中的工作方式以及您是否想在自己的項目中使用它有所了解。
Annotations
函數注釋
對于函數,您可以批注參數和返回值。這是按如下方式完成的:
def func(arg: arg_type, optarg: arg_type = default) -> return_type:
...
對于參數,語法為 ,而返回類型使用 進行批注。請注意,注釋必須是有效的 Python 表達式。
argument: annotation``-> annotation
示例:
import math
def circumference(radius: float) -> float:
return 2 * math.pi * radius
運行代碼時,還可以檢查批注。它們存儲在函數的特殊屬性中:.__annotations__
>>> circumference(1.23)
7.728317927830891
>>> circumference.__annotations__
{'radius': <class 'float'>, 'return': <class 'float'>}
變量注釋
在上一節的定義中,您只對參數和返回值進行了批注。您沒有在函數體內添加任何注釋。通常情況下,這就足夠了。circumference()
但是,有時類型檢查器也需要幫助來確定變量的類型。變量注釋在 PEP 526 中定義,并在 Python 3.6 中引入。語法與函數參數注釋相同:
pi: float = 3.142
def circumference(radius: float) -> float:
return 2 * pi * radius
變量已使用類型提示進行批注:pi:float
注:靜態類型檢查器能夠確定這是一個浮點數,因此在此示例中不需要注釋
您可以在不給變量賦值的情況下對其進行注釋。這會將注釋添加到字典中,而變量保持未定義狀態:__annotations__
>>> nothing: str
>>> nothing
NameError: name 'nothing' is not defined
>>> __annotations__
{'nothing': <class 'str'>}
由于沒有賦值,因此尚未給定義名稱
Sequences and Mapping(序列和字典)
name:str="Gudio"
pi:float=3.143
centered:bool = False
#復合類型
names:list=["Guido","Jukka","IVan"]
version:tuple=(3,7,1)
options:dict={"centered":False,"capitalize":True}
然而,這并不能真正說明全部情況.提示的信息不全面
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}
請注意,這些類型中的每一個都以大寫字母開頭,并且它們都使用方括號來定義項目類型:
該模塊還包括其它類型:Counter,Deque,FrozenSet,NamedTuple Set
#卡牌函數示意
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
元組和列表注釋方式的不同
元組是不可變的序列,通常由固定數量的可能不同類型的元素組成。例如,我們將一張牌表示為花色和等級的元組。通常,您為 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]
類型別名
示例
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])
這太可怕了!
回想一下,類型注釋是常規 Python 表達式。這意味著您可以通過將類型別名分配給新變量來定義自己的類型別名。例如,您可以創建和鍵入別名:Card``Deck
from typing import List, Tuple
Card = Tuple[str, str]
Deck = List[Card]
使用這些別名,注釋變得更具可讀性:deal_hands()
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]]
#print:它顯示的是2元字符串列表的別名
沒有返回值的函數
您可能知道沒有顯式返回的函數仍然返回 None:
>>> def play(player_name):
... print(f"{player_name} plays")
...
>>> ret_val = play("Jacob")
Jacob plays
>>> print(ret_val)
None
雖然這些函數在技術上返回一些東西,但該返回值是沒有用的。您應該添加類型提示,說明與返回類型一樣多: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
請注意,顯式表示函數不返回任何內容與不添加有關返回值的類型提示不同:
# play.py
def play(player_name: str):
print(f"{player_name} plays")
ret_val = play("Henrik")
在后一種情況下,Mypy 沒有關于返回值的信息,因此它不會生成任何警告:
$ mypy play.py
Success: no issues found in 1 source file
作為一個更奇特的情況,請注意,您還可以注釋永遠不會正常返回的函數。這是使用 NoReturn 完成的:
from typing import NoReturn
def black_hole() -> NoReturn:
raise Exception("There is no going back ...")
由于總是引發異常,因此它永遠不會正確返回。
類型Any
import random
from typing import Any, Sequence
def choose(items: Sequence[Any]) -> Any:
return random.choice(items)
這或多或少意味著它所說的:是一個可以包含任何類型的項的序列,并將返回任何類型的一個此類項。不幸的是,這并沒有那么有用。
類型論
子類型(Subtypes)
從形式上講,如果滿足以下兩個條件,我們說該類型是TU的子類型;
- 來自的每個值也在TU類型的值集中
- 類型中每個函數也都在UT類型的函數集中
這兩個條件保證即使類型與不同,類型的變量也可以始終假裝為TUTU。
子類型的重要性在于子類型始終可以偽裝成其超類型。
示例:
def double(number: int) -> int:
return number * 2
print(double(True)) # Passing in bool instead of int
Covariant, Contravariant, and Invariant
Tupleis covariant. This means that it preserves the type hierarchy of its item types: is a subtype of because is a subtype of .Tuple[bool]``Tuple[int]``bool``intListis invariant. Invariant types give no guarantee about subtypes. While all values of are values of , you can append an to and not to . In other words, the second condition for subtypes does not hold, and is not a subtype of .List[bool]``List[int]``int``List[int]``List[bool]``List[bool]``List[int]Callableis contravariant in its arguments. This means that it reverses the type hierarchy. You will see how works later, but for now think of as a function with its only argument being of type . An example of a is the function defined above. Being contravariant means that if a function operating on a is expected, then a function operating on an would be acceptable.Callable``Callable[[T], ...]``T``Callable[[int], ...]``double()``bool``int
使用Python類型
類型變量
類型變量是一種特殊變量,可以根據情況采用任何類型。
讓我們創建一個類型變量,它將有效地封裝以下行為:choose()
# choose.py
import random
from typing import Sequence, TypeVar
Choosable = TypeVar("Choosable")
def choose(items: Sequence[Choosable]) -> Choosable:
return random.choice(items)
names = ["Guido", "Jukka", "Ivan"]
reveal_type(names)
name = choose(names)
reveal_type(name)
必須從模塊中使用定義類型變量。使用時,類型變量的范圍涵蓋所有可能的類型,并采用最具體的類型。在示例中,現在是一個:TypeVar``typing``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*'

浙公網安備 33010602011771號