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 所指出的提示。
儘管型別提示可以是簡單類別,像是 float 或 str,他們也可以變得更為複雜。模組 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 的實例。在這個範例中,Vector 及 list[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]) -> None:
... # 主體
async def on_update(value: str) -> None:
... # 主體
callback: Callable[[str], Awaitable[None]] = on_update
使用下標語法 (subscription syntax) 時,必須使用到兩個值,分別為引述串列以及回傳類別。引數串列必須為一個型別串列:ParamSpec、Concatenate 或是一個刪節號 (ellipsis, ...)。回傳類別必為一個單一類別。
若刪節號字面值 ... 被當作引數串列給定,其指出一個具任何、任意參數列表的 callable 會被接受:
def concat(x: str, y: str) -> str:
return x + y
x: Callable[..., str]
x = str # OK
x = concat # 也 OK
Callable 不如有可變數量引數的函式、overloaded functions、或是僅限關鍵字參數的函式,可以表示複雜簽名。然而,這些簽名可以透過定義一個具有 __call__() 方法的 Protocol 類別進行表示:
from collections.abc import Iterable
from typing import Protocol
class Combiner(Protocol):
def __call__(self, *vals: bytes, maxlen: int | None = None) -> list[bytes]: ...
def batch_proc(data: Iterable[bytes], cb_results: Combiner) -> bytes:
for item in data:
...
def good_cb(*vals: bytes, maxlen: int | None = None) -> list[bytes]:
...
def bad_cb(*vals: bytes, maxitems: int | None) -> list[bytes]:
...
batch_proc([], good_cb) # OK
batch_proc([], bad_cb) # Error! Argument 2 has incompatible type because of
# different name and kind in the callback
Callable 物件可以取用其他 callable 當作引數使用,可以透過 ParamSpec 指出他們的參數型別是個別獨立的。另外,如果這個 callable 從其他 callable 新增或刪除引數時,將會使用到 Concatenate 運算子。他們可以分別採用 Callable[ParamSpecVariable, ReturnType] 以及 Callable[Concatenate[Arg1Type, Arg2Type, ..., ParamSpecVariable], ReturnType] 的形式。
在 3.10 版的變更: Callable 現已支援 ParamSpec 以及 Concatenate。請參閱 PEP 612 閱讀詳細內容。
也參考
ParamSpec 以及 Concatenate 的文件中,提供範例如何在 Callable 中使用。
泛型¶
因為關於物件的型別資訊留存在容器之內,且無法使用通用的方式進行靜態推論 (statically inferred),許多標準函式庫的容器類別支援以下標來表示容器內預期的元素。
from collections.abc import Mapping, Sequence
class Employee: ...
# Sequence[Employee] indicates that all elements in the sequence
# must be instances of "Employee".
# Mapping[str, str] indicates that all keys and all values in the mapping
# must be strings.
def notify_by_email(employees: Sequence[Employee],
overrides: Mapping[str, str]) -> None: ...
泛型函式及類別可以使用型別參數語法 (type parameter syntax) 進行參數化 (parameterize) :
from collections.abc import Sequence
def first[T](l: Sequence[T]) -> T: # Function is generic over the TypeVar "T"
return l[0]
或是直接使用 TypeVar 工廠 (factory):
from collections.abc import Sequence
from typing import TypeVar
U = TypeVar('U') # Declare type variable "U"
def second(l: Sequence[U]) -> U: # Function is generic over the TypeVar "U"
return l[1]
在 3.12 版的變更: 在 Python 3.12 中,泛型的語法支援是全新功能。
註釋元組 (tuple)¶
在 Python 大多數的容器當中,加註型別系統認為容器內的所有元素會是相同型別。舉例來說:
from collections.abc import Mapping
# Type checker will infer that all elements in ``x`` are meant to be ints
x: list[int] = []
# Type checker error: ``list`` only accepts a single type argument:
y: list[int, str] = [1, 'foo']
# Type checker will infer that all keys in ``z`` are meant to be strings,
# and that all values in ``z`` are meant to be either strings or ints
z: Mapping[str, str | int] = {}
list 只接受一個型別引數,所以型別檢查器可能在上述 y 賦值 (assignment) 觸發錯誤。類似的範例,Mapping 只接受兩個型別引數:第一個引數指出 keys(鍵)的型別;第二個引數指出 values(值)的型別。
然而,與其他多數的 Python 容器不同,在慣用的 (idiomatic) Python 程式碼中,元組可以擁有不完全相同型別的元素是相當常見的。為此,元組在 Python 的加註型別系統中是個特例 (special-cased)。tuple 接受任何數量的型別引數:
# OK: ``x`` is assigned to a tuple of length 1 where the sole element is an int
x: tuple[int] = (5,)
# OK: ``y`` is assigned to a tuple of length 2;
# element 1 is an int, element 2 is a str
y: tuple[int, str] = (5, "foo")
# Error: the type annotation indicates a tuple of length 1,
# but ``z`` has been assigned to a tuple of length 3
z: tuple[int] = (1, 2, 3)
為了標示一個元組可以為任意長度,且所有元素皆是相同型別 T,請使用 刪節號字面值 (literal ellipsis) ...:tuple[T, ...]。為了標示一個空元組,請使用 tuple[()]。單純使用 tuple 作為註釋會與使用 tuple[Any, ...] 相等:
x: tuple[int, ...] = (1, 2)