在Python語言發展的過程中,PEP提案發揮了巨大的作用,如PEP 3107 和 PEP 484提案,分別給我們帶來了函式註解(Function Annotations)和型別提示(Type Hints)的功能。
PEP 3107:定義了函式註解的語法,允許為函式的引數和返回值新增後設資料註解。
PEP 484:按照PEP 3107函式註解的語法,從Python語法層面全面支援型別提示,型別提示可以是內建型別、內建類、抽象基類、types模組中提供的型別和開發人員自定義的類。
另外 PEP 526, PEP 544, PEP 586, PEP 589, PEP 591 這些東西對 PEP 3107 和 PEP 484 進行了補充,比如新增了變數註釋,字面量註釋這些東西。
需要注意的是,型別提示僅有提示的作用,這裡的提示是指使用者閱讀Python程式碼的時候的提示,僅在語法層面支援,對程式碼的執行沒有任何影響,Python 直譯器在執行程式碼的時候會忽略型別提示,也就是說,Python的型別提示僅是為了提升程式碼可讀性,一定程度上緩解"動態語言一時爽,程式碼重構火葬場"的尷尬。
下面將函式註解和型別提示,統稱為型別註解。
型別註解優點
1、可以使Python擁有部分靜態語言的特性,利用型別註解可以實現一種類似型別宣告的效果,提升程式碼的可讀性及後續的可維護性。
2、型別註解可以讓IDE(如pycharm)像靜態語言那樣分析我們的程式碼,及時給我們相應的提示,如下圖對比:
3、多多使用型別註解,不僅可以讓Python擁有強型別語言的嚴謹,還能保持Python作為動態型別語言的靈活性。
普通變數型別註解
在宣告變數時,變數的後面可以加一個冒號,後面再寫上變數的型別,如 int、list 等等,以此實現型別註解。
a: int = 22
b: str = "name"
c: float = 55.5
d: bool = True
e: list = [1, 2, 3]
f: set = {1, 2, 3}
g: dict = {"name": "ming", "age": 22}
h: tuple = (1, 2, 3)
i: bytes = b'world'
j: bytearray = bytearray("world")
函式引數及返回值型別
函式引數的型別宣告就是冒號+型別即可,和普通變數型別宣告沒區別。
函式返回值的型別宣告是用箭頭指向具體的型別,如果是返回值有多個,使用元組包裹即可(因為函式的多個返回值就是以元組形式返回的),需要注意的是,箭頭左右兩邊都要留有空格。
def handler(a: int, b: int) -> int:
return a + b
def handler2(a: int, b: int, *args: int) -> int:
return a + b + sum(args)
def handler3(a: int, b: int, *args: int, **kwargs: int) -> (int, str):
return a + b + sum(args) + sum(kwargs.values()), ""
typing模組
typing模組的加入不會影響程式的執行,也不會報正式的錯誤,pycharm支援檢測基於typing註解的錯誤,不符合規定型別註解時會出現黃色警告,但不會影響程式執行。
容器型別 & 複合型別
列表、字典、元組等包含元素的複合型別,用簡單的 list,dict,tuple 不能夠明確說明內部元素的具體型別。
此外,Python本身就是動態型別的語言,如果我們強制使用某種型別,一定程度上會喪失Python作為動態語言的優勢,因此 typing 模組提供了一種複合型別註解的語法,即一個引數即可以是型別A,也可以是型別B或者型別C
from typing import Dict, List, Set, Tuple, Union
# 字典
d: Dict[str, int] = {"a": 1, "b": 2}
d1: Dict[str, int or str] = {"a": 1, "b": "2"} # 使用or表示支援多個型別
# 列表
l: List[int] = [1, 2, 3]
l1: List[int or str] = [1, 2, "3"]
# 元組
t: Tuple[str, int] = ("a", 1) # 代表了構成元組的第一個元素是 str 型別,第二個元素是 int 型別
t1: Tuple[str, ...] = ("a", "b", "c", "d", "e", "f", "g") # 代表接受多個 str 型別的元素
t2: Tuple[str or int, ...] = ("a", "b", 2) # 代表接受多個 str 或 int 型別的元素
# 集合
s: Set[int] = {1, 2, 3, 4}
s1: Set[Union[int, str, float]] = {1, "2", 3.333, 4} # Union 同 or
TypedDict
TypedDict宣告一個字典型別,該型別期望它的所有例項都有一組固定的keys,其中每個key都與對應型別的值關聯。
from typing import TypedDict
class Student(TypedDict):
name: str
age: int
height: float
s1: Student = {
"name": "xiao ming",
"age": 22,
"height": 55.5
}
s2: Student = {
"name": "xiao hong",
"age": 21,
}
可以看出,pycharm也會警告我們字典例項中缺失的key。
同時,在我們生成字典例項的時候,pycharm也會給我們key的提示。
型別別名
型別別名是透過將型別分配給別名來定義的,型別別名可用於簡化複雜型別提示。
from typing import Union
Number = Union[int, float]
def process(v: Number) -> Number:
return v
x: Number = 2
y: Number = 2.2
process(x)
process(22) # 型別檢查成功,型別別名和原始型別是等價的
NewType
使用NewType輔助類來建立不同的型別
from typing import NewType
Number = NewType("Number", int)
def process(v: Number) -> Number:
return v
x: Number = Number(22)
process(x)
process(22) # 型別檢查異常:Expected type 'Number', got 'int' instead
# 原因就是NewType建立的是原始型別的“子型別”
因此,型別別名 和 NewType 具體使用哪個,要視情況而定,不知道使用哪個,可以先使用型別別名。
NoReturn
當一個方法沒有返回結果時,為了註解它的返回型別,我們可以將其註解為 NoReturn。
因為Python 的函式執行結束時隱式返回 None
,這和真正的無返回值是有區別的。
from typing import NoReturn
def process() -> NoReturn:
pass
可選型別:Optional
使用 Optional[]
表示可能為 None 的值
from typing import Optional
def handler(x: int) -> Optional[int]:
if x % 2 == 0:
return x
可呼叫物件:Callable
若一個變數型別是可呼叫函式,則可以用 Callable[[Arg1Type, Arg2Type], ReturnType]
實現型別提示
from typing import Optional, Callable
def handler(x: int) -> Optional[int]:
if x % 2 == 0:
return x
def handler2(func: Callable[[int], Optional[int]]):
pass
handler2(handler)
字面量:Literal
指示相應的變數或函式引數只接收與提供的字面量(或多個字面量之一)等效的值,可以理解為規定了某個引數或變數的所有列舉值。
from typing import Literal, NoReturn
Mode = Literal["r", "w"]
def process(mode: Mode) -> NoReturn:
pass
process("s")
可以看出,pycharm檢查出了我們輸入的值並不符合字面量規定的值,進而出現了黃色警告。
Any
是一種特殊的型別,每種型別都視為與Any相容,同樣,Any也與所有型別相容。可以對Any型別的值執行任何操作或方法呼叫,並將其分配給任何變數。將Any型別的值分配給更精確的型別(more precise type)時,不會執行型別檢查,所有沒有返回型別或引數型別的函式都將隱式地預設使用Any。
使用Any,說明值是動態型別。
把所有的型別都註解為 Any
將毫無意義,因此 Any
應當儘量少使用
from typing import Any
def foo() -> Any:
pass
抽象基類
# 在某些情況下,我們可能並不需要嚴格區分一個變數或引數到底是列表 list 型別還是元組 tuple 型別
# 可以使用一個更為泛化的型別,叫做 Sequence,其用法類似於 List
class typing.Sequence(Reversible[T_co], Collection[T_co])
# collections.abc.Iterator的泛型版本
# 註釋函式引數中的迭代型別時,推薦使用的抽象集合型別
class typing.Iterable(Generic[T_co])
def print_iterable(x: Iterable):
for i in x:
print(i)
# collections.abc.Mapping的泛型(generic)版本
# 註釋函式引數中的Key-Value型別時,推薦使用的抽象集合型別
class typing.Mapping(Sized, Collection[KT], Generic[VT_co])
泛型:TypeVar
先丟擲問題:
假設有一個函式,要求它既能夠處理字串,又能夠處理數字。那麼你可能很自然地想到了 Union
,如下:
from typing import Union
AddValue = Union[int, str]
def add(a: AddValue, b: AddValue) -> AddValue:
return a + b
if __name__ == "__main__":
print(add(1, 2)) # 型別檢查透過,輸出 3
print(add("1", "2")) # 型別檢查透過,輸出 12
print(add("1", 2)) # 型別檢查透過,報錯 TypeError: can only concatenate str (not "int") to str
在型別檢查透過的情況下,我們完成並執行了這段程式碼,可是程式碼卻報錯了!
原因就是我們的初衷是數字和數字相加實現求和,字串和字串相加實現拼接,沒有考慮到字串與數字混用的問題,從而引發錯誤。
根據以上問題,我們可以引入泛型來解決這個問題:
from typing import TypeVar
AddT = TypeVar("AddT", int, str)
def add(a: AddT, b: AddT) -> AddT:
return a + b
if __name__ == "__main__":
print(add(1, 2)) # 型別檢查透過,輸出 3
print(add("1", "2")) # 型別檢查透過,輸出 12
print(add("1", 2)) # 型別檢查失敗,pycharm告警 Expected type 'str' (matched generic type 'AddT'), got 'int' instead
"""
透過告警,我們提前發現了混用型別的問題,避免了程式執行時發生異常的可能。
"""
泛型很巧妙地對型別進行了引數化,同時又保留了函式處理不同型別時的靈活性。