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)
# These reassignments are OK: ``tuple[int, ...]`` indicates x can be of any length
x = (1, 2, 3)
x = ()
# This reassignment is an error: all elements in ``x`` must be ints
x = ("foo", "bar")
# ``y`` can only ever be assigned to an empty tuple
y: tuple[()] = ()
z: tuple = ("foo", "bar")
# These reassignments are OK: plain ``tuple`` is equivalent to ``tuple[Any, ...]``
z = (1, 2, 3)
z = ()
類別物件的型別¶
一個變數被註釋為 C 可以接受一個型別為 C 的值。相對的,一個變數備註解為 type[C] (或已棄用的 typing.Type[C])可以接受本身為該類別的值 -- 具體來說,他可能會接受 C 的類別物件。舉例來說:
a = 3 # Has type ``int``
b = int # Has type ``type[int]``
c = type(a) # Also has type ``type[int]``
請記得 type[C] 是共變 (covariant) 的:
class User: ...
class ProUser(User): ...
class TeamUser(User): ...
def make_new_user(user_class: type[User]) -> User:
# ...
return user_class()
make_new_user(User) # OK
make_new_user(ProUser) # Also OK: ``type[ProUser]`` is a subtype of ``type[User]``
make_new_user(TeamUser) # Still fine
make_new_user(User()) # Error: expected ``type[User]`` but got ``User``
make_new_user(int) # Error: ``type[int]`` is not a subtype of ``type[User]``
type 僅有的合法參數是類別、Any、型別變數以及這些型別任意組合成的聯集。舉例來說:
def new_non_team_user(user_class: type[BasicUser | ProUser]): ...
new_non_team_user(BasicUser) # OK
new_non_team_user(ProUser) # OK
new_non_team_user(TeamUser) # Error: ``type[TeamUser]`` is not a subtype
# of ``type[BasicUser | ProUser]``
new_non_team_user(User) # Also an error
type[Any] 等價於 type ,其為 Python metaclass 階層結構 (hierachy)。
Annotating generators and coroutines¶
A generator can be annotated using the generic type
Generator[YieldType, SendType, ReturnType].
For example:
def echo_round() -> Generator[int, float, str]:
sent = yield 0
while sent >= 0:
sent = yield round(sent)
return 'Done'
Note that unlike many other generic classes in the standard library,
the SendType of Generator behaves
contravariantly, not covariantly or invariantly.
The SendType and ReturnType parameters default to None:
def infinite_stream(start: int) -> Generator[int]:
while True:
yield start
start += 1
It is also possible to set these types explicitly:
def infinite_stream(start: int) -> Generator[int, None, None]:
while True:
yield start
start += 1
Simple generators that only ever yield values can also be annotated
as having a return type of either
Iterable[YieldType]
or Iterator[YieldType]:
def infinite_stream(start: int) -> Iterator[int]:
while True:
yield start
start += 1
Async generators are handled in a similar fashion, but don't
expect a ReturnType type argument
(AsyncGenerator[YieldType, SendType]).
The SendType argument defaults to None, so the following definitions
are equivalent:
async def infinite_stream(start: int) -> AsyncGenerator[int]:
while True:
yield start
start = await increment(start)
async def infinite_stream(start: int) -> AsyncGenerator[int, None]:
while True:
yield start
start = await increment(start)
As in the synchronous case,
AsyncIterable[YieldType]
and AsyncIterator[YieldType] are
available as well:
async def infinite_stream(start: int) -> AsyncIterator[int]:
while True:
yield start
start = await increment(start)
Coroutines can be annotated using
Coroutine[YieldType, SendType, ReturnType].
Generic arguments correspond to those of Generator,
for example:
from collections.abc import Coroutine
c: Coroutine[list[str], str, int] # Some coroutine defined elsewhere
x = c.send('hi') # Inferred type of 'x' is list[str]
async def bar() -> None:
y = await c # Inferred type of 'y' is int
使用者定義泛型型別¶
一個使用者定義的類別可以被定義成一個泛型類別。
from logging import Logger
class LoggedVar[T]:
def __init__(self, value: T, name: str, logger: Logger) -> None:
self.name = name
self.logger = logger
self.value = value
def set(self, new: T) -> None:
self.log('Set ' + repr(self.value))
self.value = new
def get(self) -> T:
self.log('Get ' + repr(self.value))
return self.value
def log(self, message: str) -> None:
self.logger.info('%s: %s', self.name, message)
這個語法指出類別 LoggedVar 透過一個單一的 型別變數 T 進行參數化 (parameterised)。這使得 T 在類別中有效的成為型別。
泛型類別隱性繼承了 Generic。為了相容 Python 3.11 及更早版本,也可以明確的繼承 Generic 並指出是一個泛型類別:
from typing import TypeVar, Generic
T = TypeVar('T')
class LoggedVar(Generic[T]):
...
泛型類別有 __class_getitem__() 方法,其意味著可以在 runtime 進行參數化(如下述的 LoggedVar[int]):
from collections.abc import Iterable
def zero_all_vars(vars: Iterable[LoggedVar[int]]) -> None:
for var in vars:
var.set(0)
一個泛型型別可以有任意數量的型別變數。所有種類的 TypeVar 都可以作為泛型型別的參數:
from typing import TypeVar, Generic, Sequence
class WeirdTrio[T, B: Sequence[bytes], S: (int, str)]:
...
OldT = TypeVar('OldT', contravariant=True)
OldB = TypeVar('OldB', bound=Sequence[bytes], covariant=True)
OldS = TypeVar('OldS', int, str)
class OldWeirdTrio(Generic[OldT, OldB, OldS]):
...
Generic 的每個型別變數引數必不相同。因此以下是無效的:
from typing import TypeVar, Generic
...
class Pair[M, M]: # SyntaxError
...
T = TypeVar('T')
class Pair(Generic[