深入了解Python中類型檢查的終極指南
在本指南中,你將深入了解 Python 的類型檢查機制。傳統(tǒng)上,Python 解釋器以一種靈活但隱式的方式來處理類型。而近年來的 Python 版本允許你顯式地添加類型提示(type hints),這些提示可以被各種工具利用,從而幫助你更高效地開發(fā)代碼。
在本教程中,你將學習以下內(nèi)容:
- 類型注解(Type annotations)與類型提示(type hints)
- 如何為代碼(包括你自己的代碼和他人的代碼)添加靜態(tài)類型
- 如何運行靜態(tài)類型檢查器
- 如何在運行時強制執(zhí)行類型約束
這是一份內(nèi)容詳盡的指南,涵蓋范圍較廣。如果你只是想快速了解 Python 中類型提示的基本用法,并判斷類型檢查是否值得引入到你的項目中,那么你并不需要通讀全文。其中“初識類型(Hello Types)”和“優(yōu)缺點分析(Pros and Cons)”這兩個章節(jié),就能讓你初步體會到類型檢查的工作方式,并為你提供何時使用它更為合適的建議。
類型系統(tǒng)(Type Systems)
所有編程語言都包含某種形式的類型系統(tǒng),用于形式化地規(guī)定該語言可以處理哪些類別的對象,以及如何對待這些類別。例如,一個類型系統(tǒng)可以定義“數(shù)值類型”,而數(shù)字 42 就是數(shù)值類型對象的一個具體實例。
動態(tài)類型(Dynamic Typing)
Python 是一門動態(tài)類型語言。這意味著 Python 解釋器僅在代碼運行時才進行類型檢查,并且變量的類型在其生命周期內(nèi)是可以改變的。以下兩個簡單示例展示了 Python 的動態(tài)類型特性:
>>> if False: ... 1 + "two" # 這一行永遠不會執(zhí)行,因此不會拋出 TypeError ... else: ... 1 + 2 ... 3 >>> 1 + "two" # 現(xiàn)在這行會被執(zhí)行并進行類型檢查,于是拋出 TypeError TypeError: unsupported operand type(s) for +: 'int' and 'str'
在第一個例子中,分支 1 + “two” 永遠不會被執(zhí)行,因此也永遠不會被類型檢查。第二個例子則表明,當表達式 1 + “two” 被求值時,會拋出 TypeError,因為在 Python 中不能將整數(shù)和字符串相加。
接下來,我們看看變量是否可以改變其類型:
>>> thing = "Hello" >>> type(thing) <class 'str'> >>> thing = 28.1 >>> type(thing) <class 'float'>
type() 函數(shù)返回一個對象的類型。上述例子清楚地表明,變量 thing 的類型是可以改變的,而且 Python 能夠在類型變化時正確推斷出當前的類型。
靜態(tài)類型(Static Typing)
與動態(tài)類型相對的是靜態(tài)類型。靜態(tài)類型檢查是在程序運行之前進行的,通常在編譯階段完成。在大多數(shù)靜態(tài)類型語言(如 C 和 Java)中,類型檢查正是在編譯過程中完成的。
在靜態(tài)類型語言中,變量通常不允許在后續(xù)更改其類型,盡管某些語言可能提供類型轉(zhuǎn)換(casting)機制,允許將變量顯式轉(zhuǎn)換為其他類型。
讓我們看一個靜態(tài)類型語言的簡短示例。以下是 Java 中的一段代碼:
String thing; thing = "Hello";
第一行聲明了變量名 thing 在編譯時就被綁定到 String 類型。此后,該名稱不能再被重新綁定到其他類型。第二行給 thing 賦了一個值,這個值必須是一個 String 對象。例如,如果你之后寫 thing = 28.1f,編譯器就會報錯,因為浮點數(shù)與 String 類型不兼容。
Python 始終會是一門動態(tài)類型語言。不過,PEP 484 引入了類型提示(type hints),使得對 Python 代碼進行靜態(tài)類型檢查成為可能。
需要特別注意的是:與大多數(shù)靜態(tài)類型語言不同,Python 中的類型提示本身不會讓解釋器強制執(zhí)行類型約束。正如其名所示,類型提示只是“提示”——它們并不改變 Python 的運行時行為。真正執(zhí)行靜態(tài)類型檢查的是其他工具(稍后你會看到),這些工具會利用類型提示來分析代碼。
鴨子類型(Duck Typing)
在討論 Python 時,另一個經(jīng)常出現(xiàn)的概念是“鴨子類型”。這一說法源自一句諺語:“如果它走起來像鴨子,叫起來也像鴨子,那它就是一只鴨子”(或其各種變體)。
鴨子類型是一種與動態(tài)類型相關的理念:對象的具體類型或所屬類并不重要,重要的是它提供了哪些方法。使用鴨子類型時,你完全不需要檢查類型,而是檢查對象是否具有某個特定的方法或?qū)傩浴?/p>
舉個例子,你可以在任何定義了 .len() 方法的 Python 對象上調(diào)用 len():
>>> class TheHobbit: ... def __len__(self): ... return 95022 ... >>> the_hobbit = TheHobbit() >>> len(the_hobbit) 95022
注意,對 len() 的調(diào)用實際上返回的是 .len() 方法的返回值。事實上,len() 的內(nèi)部實現(xiàn)本質(zhì)上等價于:
def len(obj):
return obj.__len__()
因此,要調(diào)用 len(obj),對 obj 唯一真正的約束就是它必須定義了 .len() 方法。除此之外,obj 可以是任意類型——無論是 str、list、dict,還是我們自定義的 TheHobbit 類。
在對 Python 代碼進行靜態(tài)類型檢查時,鴨子類型可以通過結(jié)構(gòu)化子類型(structural subtyping) 得到一定程度的支持。關于鴨子類型的更多內(nèi)容,我們將在后文進一步探討。
初識類型(Hello Types)
在本節(jié)中,你將學習如何為函數(shù)添加類型提示。下面這個函數(shù)的作用是將一段文本字符串轉(zhuǎn)換成標題形式:它會正確地進行首字母大寫,并添加一條裝飾性的分隔線:
def headline(text, align=True):
if align:
return f"{text.title()}\n{'-' * len(text)}"
else:
return f" {text.title()} ".center(50, "o")
默認情況下,該函數(shù)返回一個左對齊的標題,并在其下方加上一條由連字符組成的下劃線。如果將 align 參數(shù)設為 False,則標題會被居中顯示,并用字母 o 構(gòu)成上下包圍的裝飾線:
>>> print(headline("python type checking"))
Python Type Checking
--------------------
>>> print(headline("python type checking", align=False))
oooooooooooooo Python Type Checking oooooooooooooo
現(xiàn)在,是時候添加我們的第一個類型提示了!要為函數(shù)補充類型信息,只需對其參數(shù)和返回值進行注解即可:
def headline(text: str, align: bool = True) -> str:
...
這里的 text: str 表示參數(shù) text 應該是 str 類型;同樣,可選參數(shù) align 應為 bool 類型,默認值為 True;而 -> str 則說明 headline() 函數(shù)將返回一個字符串。
關于代碼風格,PEP 8 建議如下:
- 冒號前后遵循常規(guī)規(guī)則:冒號前無空格,冒號后有一個空格,例如 text: str。
- 當參數(shù)注解與默認值同時出現(xiàn)時,在等號 = 兩側(cè)加空格:align: bool = True。
- 在返回類型箭頭 -> 兩側(cè)也應有空格:def headline(…) -> str。
需要強調(diào)的是,像這樣添加類型提示不會對程序運行時產(chǎn)生任何影響——它們僅僅是提示,Python 解釋器本身并不會強制執(zhí)行這些類型約束。例如,如果我們給(命名不太恰當?shù)模゛lign 參數(shù)傳入一個錯誤的類型,代碼依然能正常運行,沒有任何報錯或警告:
>>> print(headline("python type checking", align="left"))
Python Type Checking
--------------------
注意:這段代碼看似“有效”,是因為字符串 “left” 在布爾上下文中被視為真值(truthy)。但如果你傳入 align=“center”,雖然 “center” 同樣是真值,卻無法實現(xiàn)你預期的居中效果,反而會造成邏輯混淆。
要捕獲這類錯誤,就需要使用靜態(tài)類型檢查器——即一種在不實際運行代碼的情況下分析代碼類型是否正確的工具。
你可能已經(jīng)在編輯器中內(nèi)置了這樣的檢查器。例如,PyCharm 會立即給出警告:
Expected type 'bool', got 'str' instead
不過,最常用的類型檢查工具是 mypy。稍后你會看到 mypy 的簡要介紹,后續(xù)章節(jié)還會深入講解其工作原理。
如果你系統(tǒng)中尚未安裝 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"))
這基本上就是前面展示過的代碼:包含 headline() 的定義以及兩次調(diào)用示例。
現(xiàn)在,用 mypy 檢查這段代碼:
$ mypy headlines.py
headlines.py:10: error: Argument "align" to "headline" has incompatible
type "str"; expected "bool"
根據(jù)類型提示,mypy 明確指出:第 10 行傳給 headline 的 align 參數(shù)類型不匹配——你傳入了 str,但函數(shù)期望的是 bool。
要修復這個問題,你需要修改傳入 align 的值。此外,也可以將參數(shù)名 align 改為更清晰、不易誤解的名字,比如 centered:
# headlines.py
def headline(text: str, centered: bool = False) -> str:
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))
這里我們將 align 改為 centered,并在調(diào)用時正確傳入布爾值 True?,F(xiàn)在再運行 mypy:
$ mypy headlines.py
Success: no issues found in 1 source file
這條成功消息表明:mypy 未檢測到任何類型錯誤。(舊版本的 mypy 在無錯誤時會直接靜默輸出,不顯示任何內(nèi)容。)
最后,運行程序本身,你會看到預期的輸出:
$ python headlines.py
Python Type Checking
--------------------
oooooooooooooooooooo Use Mypy oooooooooooooooooooo
第一個標題左對齊,第二個標題居中顯示——一切如預期般工作。
優(yōu)缺點分析(Pros and Cons)
上一節(jié)讓你初步體驗了 Python 中類型檢查的實際效果。你也看到了為代碼添加類型的一個明顯優(yōu)勢:類型提示有助于捕獲某些錯誤。除此之外,還有其他幾項重要優(yōu)點:
- 類型提示有助于文檔化你的代碼:傳統(tǒng)上,如果你希望說明函數(shù)參數(shù)的預期類型,通常會使用文檔字符串(docstrings)。雖然這可行,但由于 PEP 257 并未為 docstring 中的類型描述建立統(tǒng)一標準,這類信息難以被工具自動解析和用于靜態(tài)檢查。
- 類型提示能顯著提升 IDE 和代碼檢查工具(linter)的能力:它們讓工具更容易對代碼進行靜態(tài)推理。例如,有了類型注解后,PyCharm 就知道 text 是一個字符串,從而提供更精準的代碼補全建議.
- 類型提示有助于構(gòu)建和維護更清晰的代碼架構(gòu):編寫類型提示的過程會促使你主動思考程序中各部分的數(shù)據(jù)類型。盡管 Python 的動態(tài)特性是其強大之處,但有意識地審視自己是否過度依賴鴨子類型、方法重載或多類型返回值,是一種良好的工程習慣。
當然,靜態(tài)類型檢查并非完美無缺,也存在一些需要權衡的缺點:
- 添加類型提示需要額外的開發(fā)時間和精力:雖然長遠來看可能減少調(diào)試時間,但在編寫代碼時確實會增加輸入成本。
- 類型提示在較新的 Python 版本中效果最佳:類型注解最早在 Python 3.0 引入,Python 2.7 可通過類型注釋(type comments)實現(xiàn)有限支持。但像變量注解(variable annotations)和類型提示的延遲求值(postponed evaluation)等關鍵改進,直到 Python 3.6 甚至 3.7 才真正成熟。因此,在舊版本中使用類型提示體驗會打折扣。
- 類型提示會帶來輕微的啟動性能開銷:如果你在代碼中導入了 typing 模塊,其導入時間可能較為顯著——尤其在短小腳本中更為明顯。
那么,你是否應該在自己的項目中使用靜態(tài)類型檢查呢?其實,這并不是一個“全有或全無”的選擇。幸運的是,Python 支持漸進式類型(gradual typing) 的理念:你可以逐步為代碼添加類型提示。靜態(tài)類型檢查器會忽略沒有類型注解的部分,因此你可以先從關鍵模塊開始引入類型,只要它對你有價值,就繼續(xù)推進。
回顧上述優(yōu)缺點列表,你會發(fā)現(xiàn):添加類型提示對程序的運行行為和最終用戶完全沒有任何影響。類型檢查的唯一目的,就是讓你作為開發(fā)者的工作更輕松、更高效。
以下是一些實用的經(jīng)驗法則,幫助你判斷是否應在項目中使用類型提示:
- 如果你剛開始學習 Python,完全可以暫緩使用類型提示,等積累更多經(jīng)驗后再考慮。
- 對于一次性使用的短腳本,類型提示帶來的價值非常有限。
- 對于會被他人使用的庫(尤其是發(fā)布到 PyPI 的庫),類型提示極具價值。其他項目在使用你的庫時,依賴這些類型提示才能進行完整的類型檢查。已采用類型提示的知名項目包括 cursive_re、black、Real Python 自家的 Reader 應用,以及 mypy 本身。
- 在大型項目中,類型提示能幫助你理清類型在代碼中的流動邏輯,強烈推薦使用;如果項目涉及多人協(xié)作,其價值則更加突出。
正如 Bernát Gábor 在其精彩文章《The State of Type Hints in Python》中所建議的:“只要值得編寫單元測試的地方,就應該使用類型提示。” 事實上,類型提示在代碼中扮演的角色與測試類似——它們都是幫助你寫出更健壯、更可維護代碼的輔助手段。
希望你現(xiàn)在對 Python 中的類型檢查機制有了清晰的理解,并能判斷它是否適合你的項目。
注解(Annotations)
注解(Annotations)最早在 Python 3.0 中引入,最初并沒有特定用途,僅僅是一種將任意表達式與函數(shù)參數(shù)和返回值關聯(lián)起來的機制。
多年后,PEP 484 基于 Jukka Lehtosalo 在其博士項目(即 mypy)中的工作,正式定義了如何在 Python 代碼中添加類型提示(type hints)。而實現(xiàn)類型提示的主要方式,正是通過注解。隨著類型檢查日益普及,注解如今也應主要保留用于類型提示這一目的。
接下來的幾節(jié)將詳細解釋在類型提示上下文中,注解是如何工作的。
函數(shù)注解(Function Annotations)
對于函數(shù),你可以為參數(shù)和返回值添加注解。語法如下:
def func(arg: arg_type, optarg: arg_type = default) -> return_type:
...
- 參數(shù)注解使用 參數(shù)名: 注解 的形式;
- 返回值注解則使用 -> 注解 的形式。
需要注意的是,注解必須是合法的 Python 表達式。
下面是一個簡單示例,為計算圓周長的函數(shù)添加了類型注解:
import math
def circumference(radius: float) -> float:
return 2 * math.pi * radius
運行代碼時,你還可以查看這些注解。它們被存儲在函數(shù)的一個特殊屬性 annotations 中:
>>> circumference(1.23)
7.728317927830891
>>> circumference.__annotations__
{'radius': <class 'float'>, 'return': <class 'float'>}
有時你可能會疑惑 mypy 是如何理解你的類型提示的。為此,mypy 提供了兩個特殊的調(diào)試工具:reveal_type() 和 reveal_locals()。你可以在運行 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 也能正確推斷出內(nèi)置常量 math.pi 以及局部變量 radius 和 circumference 的類型。
變量注解(Variable Annotations)
在上一節(jié)的 circumference() 函數(shù)中,我們只注解了參數(shù)和返回值,并未在函數(shù)體內(nèi)添加任何變量注解——這通常已經(jīng)足夠。
但有時,類型檢查器也需要幫助來推斷變量的類型。為此,PEP 526 在 Python 3.6 中引入了變量注解,其語法與函數(shù)參數(shù)注解一致:
pi: float = 3.142
def circumference(radius: float) -> float:
return 2 * pi * radius
這里,變量 pi 被注解為 float 類型。
注意:靜態(tài)類型檢查器完全可以從字面量 3.142 推斷出它是 float,因此這個例子中的注解并非必需。隨著你對 Python 類型系統(tǒng)了解加深,會遇到更多真正需要變量注解的場景。
變量注解會被存儲在模塊級別的 annotations 字典中:
>>> circumference(1)
6.284
>>> __annotations__
{'pi': <class 'float'>}
你甚至可以只聲明注解而不賦值。此時,注解會被加入 annotations,但變量本身并未定義:
>>> nothing: str
>>> nothing
NameError: name 'nothing' is not defined
>>> __annotations__
{'nothing': <class 'str'>}
由于沒有給 nothing 賦值,該名稱在運行時并不存在。
類型注釋(Type Comments)
如前所述,注解是在 Python 3 中引入的,并未向后兼容到 Python 2。因此,如果你的代碼需要支持舊版 Python(如 2.7),就無法使用注解。
這時可以使用類型注釋(type comments) ——一種特殊格式的注釋,用于在舊代碼中添加類型提示。
例如,為函數(shù)添加類型注釋的方式如下:
import math
def circumference(radius):
# type: (float) -> float
return 2 * math.pi * radius
類型注釋本質(zhì)上只是普通注釋,因此可在任何 Python 版本中使用。
但請注意:類型注釋由類型檢查器直接處理,不會出現(xiàn)在 annotations 字典中:
>>> circumference.__annotations__
{}
類型注釋必須以 # type: 開頭,并且必須位于函數(shù)定義的同一行或下一行。如果函數(shù)有多個參數(shù),用逗號分隔各類型:
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))
你也可以為每個參數(shù)單獨寫一行注釋:
# 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 headlines.py Success: no issues found in 1 source file
如果出現(xiàn)類型錯誤(例如第 10 行調(diào)用 headline() 時傳入 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 類型。
那么,該用注解還是類型注釋
當你為自己的代碼添加類型提示時,應該選擇注解還是類型注釋?
簡短回答:能用注解就用注解,只有在必須兼容舊版本時才用類型注釋。
- 注解語法更簡潔,將類型信息緊貼代碼,可讀性更強。它們是官方推薦的類型提示方式,未來也會持續(xù)得到支持和改進。
- 類型注釋則更冗長,還可能與代碼中的其他注釋(如 linter 指令)產(chǎn)生沖突。但它們適用于不支持注解的舊代碼庫。
玩轉(zhuǎn) Python 類型(第一部分)
到目前為止,你只在類型提示中使用了像 str、float 和 bool 這樣的基本類型。但實際上,Python 的類型系統(tǒng)非常強大,支持許多更復雜的類型。這是必要的——因為 Python 本質(zhì)上是動態(tài)的、基于鴨子類型的語言,類型系統(tǒng)必須能夠合理地建模這種靈活性。
在本節(jié)中,你將通過實現(xiàn)一個簡單的紙牌游戲,深入學習 Python 的類型系統(tǒng)。你會看到如何指定:
- 序列(如元組、列表)和映射(如字典)的類型
- 讓代碼更易讀的類型別名(type aliases)
- 表示函數(shù)或方法不返回任何值的方式
- 表示對象可以是任意類型的機制
在簡要探討一些類型理論之后,你還會看到更多在 Python 中指定類型的方法。本節(jié)的代碼示例可在此處找到。
示例:一副紙牌
下面的示例實現(xiàn)了一副撲克牌:
# 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):
"""創(chuàng)建一副新的 52 張牌"""
deck = [(s, r) for r in RANKS for s in SUITS]
if shuffle:
random.shuffle(deck)
return deck
def deal_hands(deck):
"""將牌平均發(fā)給四位玩家"""
return (deck[0::4], deck[1::4], deck[2::4], deck[3::4])
def play():
"""運行一個四人紙牌游戲"""
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()
- 每張牌用一個包含兩個字符串的元組表示:(花色, 點數(shù))。
- 整副牌是一個牌的列表。
- create_deck() 創(chuàng)建一副 52 張的標準牌,并可選擇是否洗牌。
- deal_hands() 將牌平均分給四位玩家。
- play() 目前只是準備游戲:洗牌并分發(fā),尚未實現(xiàn)具體玩法。
典型輸出如下:
$ 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)
現(xiàn)在,我們?yōu)榧埮朴螒蛱砑宇愋吞崾荆礊?create_deck()、deal_hands() 和 play() 添加注解。
第一個挑戰(zhàn)是:如何注解復合類型?比如表示整副牌的列表,以及表示單張牌的元組。
對于基本類型,注解很簡單:
>>> 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[2] 是 str、version[0] 是 int、options[“centered”] 是 bool。這些信息對類型檢查器來說是缺失的。
因此,應使用 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 是一個字符串列表;
- version 是一個包含三個整數(shù)的三元組;
- options 是一個從字符串映射到布爾值的字典。
typing 模塊還提供了更多復合類型,如 Counter、Deque、FrozenSet、NamedTuple、Set 等。
回到紙牌游戲:一張牌是 (str, str) 元組,整副牌是 List[Tuple[str, str]]。因此,create_deck() 可注解為:
from typing import List, Tuple
def create_deck(shuffle: bool = False) -> List[Tuple[str, str]]:
"""創(chuàng)建一副新的 52 張牌"""
deck = [(s, r) for r in RANKS for s in SUITS]
if shuffle:
random.shuffle(deck)
return deck
很多時候,函數(shù)只關心參數(shù)是“某種序列”,而不關心是列表還是元組。此時應使用 typing.Sequence:
from typing import List, Sequence
def square(elems: Sequence[float]) -> List[float]:
return [x**2 for x in elems]
這體現(xiàn)了鴨子類型思想:只要對象支持 len() 和 .getitem(),就是 Sequence。
類型別名(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]],
]:
...
簡直難以閱讀!
由于類型注解是合法的 Python 表達式,你可以定義類型別名:
from typing import List, Tuple Card = Tuple[str, str] Deck = List[Card]
現(xiàn)在,deal_hands() 的注解變得清晰:
def deal_hands(deck: Deck) -> Tuple[Deck, Deck, Deck, Deck]:
"""將牌平均發(fā)給四位玩家"""
return (deck[0::4], deck[1::4], deck[2::4], deck[3::4])
類型別名不僅提升可讀性,還能被檢查其真實含義:
>>> Deck typing.List[typing.Tuple[str, str]]
無返回值的函數(shù)(Functions Without Return Values)
沒有顯式 return 的函數(shù)實際上返回 None:
>>> def play(player_name):
... print(f"{player_name} plays")
...
>>> ret_val = play("Jacob")
Jacob plays
>>> print(ret_val)
None
雖然技術上返回了 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
如果不加 -> None,mypy 無法推斷返回類型,也就不會報錯。
進階情況:對于永遠不會正常返回的函數(shù)(如總是拋異常),應使用 NoReturn:
from typing import NoReturn
def black_hole() -> NoReturn:
raise Exception("There is no going back ...")
示例:開始出牌
下面是改進版的紙牌游戲:分發(fā)手牌后,隨機選擇起始玩家,然后輪流隨機出牌(暫無規(guī)則):
# 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:
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]:
return (deck[0::4], deck[1::4], deck[2::4], deck[3::4])
def choose(items):
"""隨機選擇并返回一個元素"""
return random.choice(items)
def player_order(names, start=None):
"""調(diào)整玩家順序,使 start 玩家先出"""
if start is None:
start = choose(names)
start_idx = names.index(start)
return names[start_idx:] + names[:start_idx]
def play() -> None:
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)
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()
現(xiàn)在,我們需要為新函數(shù) choose() 和 player_order() 添加類型提示。
Any 類型
choose() 既可用于玩家名列表,也可用于手牌列表(或其他任何序列)。一種寫法是:
from typing import Any, Sequence
def choose(items: Sequence[Any]) -> Any:
return random.choice(items)
但這會丟失類型信息。例如:
# choose.py names = ["Guido", "Jukka", "Ivan"] name = choose(names)
mypy 會推斷 names 是 List[str],但 name 的類型卻變成 Any:
$ mypy choose.py choose.py:10: error: Revealed type is 'builtins.list[builtins.str*]' choose.py:13: error: Revealed type is 'Any'
這意味著后續(xù)無法對 name 做字符串操作的類型檢查。
玩轉(zhuǎn) Python 類型(第二部分)
讓我們回到實踐。你還記得之前嘗試為通用的 choose() 函數(shù)添加類型注解時遇到的問題:
import random
from typing import Any, Sequence
def choose(items: Sequence[Any]) -> Any:
return random.choice(items)
使用 Any 的問題是:你無謂地丟失了類型信息。你知道,如果傳入一個字符串列表,choose() 應該返回一個字符串。下面我們將學習如何用類型變量(Type Variables)來精確表達這種關系,并進一步探討:
- 鴨子類型與協(xié)議(Protocols)
- 默認值為 None 的參數(shù)
- 類方法的類型注解
- 自定義類作為類型
- 可變數(shù)量參數(shù)(*args, **kwargs)
- 可調(diào)用對象(Callables)
類型變量(Type Variables)
類型變量是一種特殊的變量,它可以根據(jù)上下文“取”任意類型。
我們來為 choose() 創(chuàng)建一個類型變量,以準確捕獲其行為:
# 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)
- 類型變量必須通過 typing.TypeVar 定義。
- 它會根據(jù)實際傳入的參數(shù)“推斷”最具體的類型。
運行 mypy:
$ mypy choose.py choose.py:12: error: Revealed type is 'builtins.list[builtins.str*]' choose.py:15: error: Revealed type is 'builtins.str*'
name 現(xiàn)在被正確推斷為 str!
再看幾個例子:
# choose_examples.py from choose import choose reveal_type(choose(["Guido", "Jukka"])) # str reveal_type(choose([1, 2, 3])) # int reveal_type(choose([True, 42, 3.14])) # float reveal_type(choose(["Python", 3, 7])) # object
mypy 輸出:
builtins.str* builtins.int* builtins.float* # 因為 bool ? int ? float builtins.object* # str 和 int 無共同子類型
注意:最后兩個例子沒有報錯,但類型信息變得模糊。
約束類型變量
如果你希望 choose() 只接受同一種類型的序列(比如只接受全字符串或全數(shù)字),可以約束類型變量:
Choosable = TypeVar("Choosable", str, float)
def choose(items: Sequence[Choosable]) -> Choosable:
...
現(xiàn)在:
- choose([1, 2, 3]) → float(因為 int 是 float 的子類型)
- choose([“Python”, 3, 7]) → ? 類型錯誤!
mypy 會報錯:
Value of type variable "Choosable" cannot be "object"
在紙牌游戲中,我們可以這樣約束:
Choosable = TypeVar("Choosable", str, Card)
這樣,choose() 要么處理玩家名(str),要么處理手牌(Card),但不會混合。
總結(jié)
typing 的引入是為了讓 Python 在保持動態(tài)語言靈活性的同時,獲得靜態(tài)類型語言的安全性和工程化優(yōu)勢
常見使用場景
- 為函數(shù)參數(shù)和返回值指定預期類型
- 用精確的類型信息定義復雜的數(shù)據(jù)結(jié)構(gòu)
- 提高代碼的可讀性與可維護性
- 協(xié)助 mypy 等靜態(tài)類型檢查工具識別潛在錯誤
以上就是深入了解Python中類型檢查的終極指南的詳細內(nèi)容,更多關于Python類型檢查的資料請關注腳本之家其它相關文章!
相關文章
Python利用AutoGrad實現(xiàn)自動計算函數(shù)斜率和梯度
AutoGrad 是一個老少皆宜的 Python 梯度計算模塊。對于大學生、機器學習愛好者而言,你只需要傳遞給它Numpy這樣的標準數(shù)據(jù)庫下編寫的損失函數(shù),它就可以自動計算損失函數(shù)的導數(shù)(梯度)。本文將從普通斜率計算開始,介紹到如何只使用它來實現(xiàn)一個邏輯回歸模型2022-07-07
python實現(xiàn)高精度求自然常數(shù)e過程詳解
這篇文章主要為大家介紹了python實現(xiàn)高精度求自然常數(shù)e過程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-05-05
Python實現(xiàn)獲取nginx服務器ip及流量統(tǒng)計信息功能示例
這篇文章主要介紹了Python實現(xiàn)獲取nginx服務器ip及流量統(tǒng)計信息功能,涉及Python針對nginx服務器信息操作相關實現(xiàn)技巧,需要的朋友可以參考下2018-05-05

