Python類型註解僅在語法展示層面支持,對代碼的運行沒有任何影響,Python 解釋器在運行代碼的時候會忽略類型提示,Python的類型註解極大的提升了代碼可讀性,一定程度上緩解"動態語言一時爽,代碼重構火葬場"的尷尬。 ...
在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
"""
通過告警,我們提前發現了混用類型的問題,避免了程式運行時發生異常的可能。
"""
泛型很巧妙地對類型進行了參數化,同時又保留了函數處理不同類型時的靈活性。
引用
本文來自博客園
作者:奧森iorson