typing --- 支援型別提示

在 3.5 版被加入.

原始碼:Lib/typing.py

備註

Python runtime 不強制要求函式與變數的型別註釋。他們可以被第三方工具使用,如:型別檢查器、IDE、linter 等。


此模組提供 runtime 型別提示支援。

動腦筋思考下面的函式:

def surface_area_of_cube(edge_length: float) -> str:
    return f"The surface area of the cube is {6 * edge_length ** 2}."

函式 surface_area_of_cube 需要一個引數且預期是一個 float 的實例,如 edge_length: float 所指出的型別提示。這個函式預期會回傳一個 str 的實例,如 -> str 所指出的提示。

儘管型別提示可以是簡單類別,像是 floatstr,他們也可以變得更為複雜。模組 typing 提供一組更高階的型別提示詞彙。

新功能會頻繁的新增至 typing 模組中。typing_extensions 套件為這些新功能提供了 backport(向後移植的)版本,提供給舊版本的 Python 使用。

也參考

型別小抄 (Typing cheat sheet)

型別提示的快速預覽(發布於 mypy 的文件中)

mypy 文件的 型別系統參考資料 (Type System Reference) 章節

Python 的加註型別系統是基於 PEPs 進行標準化,所以這個參照 (reference) 應該在多數 Python 型別檢查器中廣為使用。(某些部分依然是特定給 mypy 使用。)

Python 的靜態型別 (Static Typing)

由社群編寫的跨平台型別檢查器文件 (type-checker-agnostic) 詳細描述加註型別系統的功能、實用的加註型別衍伸工具、以及加註型別的最佳實踐 (best practice)。

Python 型別系統的技術規範

關於 Python 型別系統標準的 (canonical)、最新的技術規範可以在Python 型別系統的技術規範找到。

型別別名

一個型別別名被定義來使用 type 陳述式,其建立了 TypeAliasType 的實例。在這個範例中,Vectorlist[float] 會被當作和靜態型別檢查器一樣同等對待:

type Vector = list[float]

def scale(scalar: float, vector: Vector) -> Vector:
    return [scalar * num for num in vector]

# passes type checking; a list of floats qualifies as a Vector.
new_vector = scale(2.0, [1.0, -4.2, 5.4])

型別別名對於簡化複雜的型別簽名 (complex type signature) 非常好用。舉例來說:

from collections.abc import Sequence

type ConnectionOptions = dict[str, str]
type Address = tuple[str, int]
type Server = tuple[Address, ConnectionOptions]

def broadcast_message(message: str, servers: Sequence[Server]) -> None:
    ...

# The static type checker will treat the previous type signature as
# being exactly equivalent to this one.
def broadcast_message(
    message: str,
    servers: Sequence[tuple[tuple[str, int], dict[str, str]]]
) -> None:
    ...

type 陳述式是 Python 3.12 的新功能。為了向後相容性,型別別名可以透過簡單的賦值來建立:

Vector = list[float]

或是用 TypeAlias 標記,讓它明確的表示這是一個型別別名,而非一般的變數賦值:

from typing import TypeAlias

Vector: TypeAlias = list[float]

NewType

使用 NewType 輔助工具 (helper) 建立獨特型別:

from typing import NewType

UserId = NewType('UserId', int)
some_id = UserId(524313)

若它是原本型別的子類別,靜態型別檢查器會將其視為一個新的型別。這對於幫助擷取邏輯性錯誤非常有用:

def get_user_name(user_id: UserId) -> str:
    ...

# passes type checking
user_a = get_user_name(UserId(42351))

# fails type checking; an int is not a UserId
user_b = get_user_name(-1)

你依然可以在對於型別 UserId 的變數中執行所有 int 的操作。這讓你可以在預期接受 int 的地方傳遞一個 UserId,還能預防你意外使用無效的方法建立一個 UserId

# 'output' is of type 'int', not 'UserId'
output = UserId(23413) + UserId(54341)

注意這只會透過靜態型別檢查器強制檢查。在 runtime 中,陳述式 (statement) Derived = NewType('Derived', Base) 會使 Derived 成為一個 callable(可呼叫物件),會立即回傳任何你傳遞的引數。這意味著 expression (運算式)Derived(some_value) 不會建立一個新的類別或過度引入原有的函式呼叫。

更精確地說,expression some_value is Derived(some_value) 在 runtime 永遠為 true。

這會無法建立一個 Derived 的子型別:

from typing import NewType

UserId = NewType('UserId', int)

# Fails at runtime and does not pass type checking
class AdminUserId(UserId): pass

無論如何,這有辦法基於 '衍生的' NewType 建立一個 NewType

from typing import NewType

UserId = NewType('UserId', int)

ProUserId = NewType('ProUserId', UserId)

以及針對 ProUserId 的型別檢查會如期運作。

更多細節請見 PEP 484

備註

請記得使用型別別名是宣告兩種型別是互相相等的。使用 type Alias = Original 則會讓靜態型別檢查器在任何情況之下將 Alias 視為與 Original 完全相等。這當你想把複雜的型別簽名進行簡化時,非常好用。

相反的,NewType 宣告一個型別會是另外一種型別的子類別。使用 Derived = NewType('Derived', Original) 會使靜態型別檢查器將 Derived 視為 Original 的子類別,也意味著一個型別為 Original 的值,不能被使用在任何預期接收到型別 Derived 的值的區域。這當你想用最小的 runtime 成本預防邏輯性錯誤而言,非常有用。

在 3.5.2 版被加入.

在 3.10 版的變更: 現在的 NewType 比起一個函式更像一個類別。因此,比起一般的函式,呼叫 NewType 需要額外的 runtime 成本。

在 3.11 版的變更: 呼叫 NewType 的效能已經恢復與 Python 3.9 相同的水準。

註釋 callable 物件

函式,或者是其他 callable 物件,可以使用 collections.abc.Callable 或以棄用的 typing.Callable 進行註釋。 Callable[[int], str] 象徵為一個函式,可以接受一個型別為 int 的引數,並回傳一個 str

舉例來說:

from collections.abc import Callable, Awaitable

def feeder(get_next_item: Callable[[], str]) -> None:
    ...  # 主體

def async_query(on_success: Callable[[int], None],
                on_error: Callable[[int, Exception], None