最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

深入了解Python中類型檢查的終極指南

 更新時間:2026年02月28日 08:59:09   作者:SunnyRivers  
在本指南中,你將深入了解 Python 的類型檢查機制,這是一份內(nèi)容詳盡的指南,涵蓋范圍較廣,包括如何運行靜態(tài)類型檢查器,如何在運行時強制執(zhí)行類型約束等,希望對大家有所幫助

在本指南中,你將深入了解 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類型檢查的資料請關注腳本之家其它相關文章!

相關文章

最新評論

增城市| 咸宁市| 上虞市| 丹江口市| 镇雄县| 义乌市| 二连浩特市| 乾安县| 红河县| 高陵县| 博乐市| 嘉定区| 政和县| 康定县| 呼伦贝尔市| 曲松县| 巴中市| 淅川县| 大安市| 石河子市| 腾冲县| 丽水市| 岢岚县| 灵武市| 海南省| 疏附县| 耿马| 沾益县| 西华县| 翁牛特旗| 同德县| 应城市| 巴彦淖尔市| 凤山市| 荆门市| 三穗县| 温州市| 桐梓县| 普兰县| 奇台县| 通州区|