python 類型提示
本文參考自:https://www.cnblogs.com/poloyy/p/15170297.html
寫在前面:Python 運行時不強制執行函數和變量類型注解,但這些注解可用於類型檢查器、IDE、靜態檢查器等第三方工具。(換句話說,本文下面所說的類型,都是供代碼檢查器,IDE提示用的,並不是說你設置了類型,就真的變成像 java 那樣的強類型定義語言了)
簡單入門
python 是一門動態語言,因此在你拿到一個變量之前,你無法知道一個變量到底是什么類型的,因此當你傳遞一個變量給一個函數,盡管你內心知道這個參數會是某個特定的類型,但是 IDE 不知道它會是什么類型,因此它不會給你任何的代碼提示。
因此,Python 3.5、3.6 新增了兩個特性 PEP 484 和 PEP 526,幫助 IDE 為我們提供更智能的提示(並不會影響語言本身),簡單的寫法:
# 聲明變量時使用
num: int = 1
str1: str
# 函數參數也可以使用
def test(num: int = 1):
return num
在定義一個變量時,直接在后面
: 類型
就可以給這個變量指定一個類型,后面可以繼續寫賦值語句= 1
,不寫賦值語句也不會出錯
指定函數返回值類型
def test(num: int = 1) -> int: # -> 可以指明函數的返回值類型
return num
def test(num: int = 1) -> None: # -> 可以指明函數的返回值類型
return None
在函數的冒號之前,
->
可以指定函數的返回值類型
注意,None 是一種類型提示特例
查看函數注解
我們可以通過 __annotations__
查看一個函數的注解
def test(a: str, b: int = 2) -> None:
pass
print(test.__annotations__)
打印結果:
{'a': <class 'str'>, 'b': <class 'int'>, 'return': None}
常用類型
-
int, long, float: 整型,長整形,浮點型;
-
bool, str: 布爾型,字符串類型;
-
list, tuple, dict, set: 列表,元組,字典, 集合;
-
Iterable,Iterator: 可迭代類型,迭代器類型;
-
Generator: 生成器類型;
python3.9以后可以直接使用 list, dict, set, tuple 等類型,3.9 以前可能需要
from typing import List, Dict, Set, Tuple
py3.9 需要從
from collections.abc import Iterable, Generator, Iterator
,3.9以前需要:from typing import Iterable, Generator, Iterator
list,set
list,set 類型提示,只可以給它們的內部元素指定一種類型,否則會報錯:
a: list[int] = [] # list[類型] ,list內部只能指定一種類型
a.append(1)
a.append('2') # 盡管指定了 int 類型,但是可以添加 str 類型的元素。
# --------------- 同理: set[] ----------------------------
b: set[int] = {1}
我們之前說過,類型提示只是進行提示用的,並不會真的校驗我們給它的賦值。
list,set后面跟的是中括號:
[]
tuple
tuple 可以指定多種類型。
c: tuple[int, str] = (1, '2') # 元組的第一個元素是 int 類型,第二個元素是 str 類型
如果想給 tuple 指定一種類型,但是你 tuple 的長度大於1,需要加上 ...
c: tuple[int, ...] = (1, 2, 3, 4) # 代表無論元組有多少元素,都是 int 類型
dict
dict 可以指定兩種類型:分別代表鍵,值的類型:
d: dict[str, int] = {'1': 1}
嵌套類型
各個類型之間也可以嵌套:
e: dict[str, list[int]] = {'1': [1,2]}
類型的別名
其實類型的重命名,應該屬於python的用法,不是類型提示的特殊語法:
My_type = dict[str, list[int]] # 給類型重命名
e: My_type = {'1': [1,2]}
TypeAlias 別名
和上面別名一樣,只不過顯式的說明,這個一個別名。
from typing import TypeAlias
Factors: TypeAlias = list[int] # 相當於 Factors = list[int]
x: Factors = [1,2,3]
自定義類型:NewType
和重命名有點類似,但是自定義類型相當於創建了某個類型的子類
UserId = NewType('UserId', int) # 創建一個新類型(類型名字,類型)
num: UserId = UserId('1')
print(num, type(num)) # 1 <class 'str'>
可以看出,真實的數據類型還是
str
, 說明了類型提示並不會對真實的代碼產生影響,只是IDE類型提示時會顯示該填入某種類型。
Literal
字面量類型,只接受指定的內容。
from typing import Literal
M = Literal['r','w','rb'] # 用戶輸入別的內容,會進行代碼提示
def test(mode: M):
pass
test('xb') # Expected type 'Literal['r', 'w', 'rb']', got 'Literal['xb']' instead
Optional
這個類型代表的是當前參數是一個可選參數,只能接受一種類型,Optional[X]
等同於 X | None
(或者Union[X, None]
),也就說是,它默認包含了 None:
from typing import Optional
def test(x: Optional[int]=1):
return x
Callable
Callable[[Arg1Type, Arg2Type], ReturnType]
可以用來提示一個可調用函數:
from typing import Callable
def test(a: int, b: int) -> int: # 第一個函數,接受兩個 int 參數,返回 int
return a+b
def test2(func: Callable[[int, int], int]): # 第二個函數,類型是可調用對象:這個可調用對象的參數是 2 個int, 返回值是一個 int
return func(1, 2)
test2(test)
Callable 接受兩個值,第一個值是包含可調用對象的所有參數類型的列表。第二個值是可調用對象的返回值類型。
如果調用對象沒有參數,或者有放多參數,可以這樣寫:
Callable[[], int]
無參數,返回 int
Callable[..., int]
不限定參數,返回 int
ClassVar 類變量
聲明類變量的類型
class Starship:
stats: ClassVar[dict[str, int]] = {} # class variable
damage: int = 10 # instance variable
泛型 Generic
from typing import TypeVar, Generic
T = TypeVar('T') # "T" 代表任意類型
# A 繼承自 Generic[T] ,所以這個類帶有類型
class A(Generic[T]):
def __init__(self, value: T): # 初始化值:T 類型
self.value = value
def get_value(self) -> T: # 返回 T 類型
return self.value
def set_value(self, value: T): # 設置的值:T 類型
self.value = value
def test(a: A[int]): # 注意:test 函數接受的參數,需是 A[int] 類型
print(a.value)
a.set_value('str1') # 這里會提示:Expected type 'int' (matched generic type 'T'), got 'str' instead
print(a.get_value())
a = A[str]('str1') # 實例化一個 str 類型的 A 對象
# b = A[int](2) # 也可以實例化一個 int 類型的 A 對象
test(a) # 將 str 類型的 A 對象,傳遞給 test,但是注意:test 接受的類型是 A[int]
上面的例子中,A 類繼承了
Generic[T]
,代表了 A 類是一個有類型的類。我們在實例化它時,可以隨意指定它的類型:A[int](2)
,但是一旦我們指定了某個類型,這個類中所有的T
類型 都會和實例化時的類型一致:int
因此在
test(a: A[int])
函數中,我們指定了要接受A[int]
類型的參數。而我們傳遞的卻是A[str]("str1")
對象。所以調用a.set_value("str1")
時,IDE 會提示類型不正確(但是代碼能跑通,還是之前說的,它僅僅是做類型提示,不會真的影響賦值操作)
多類型
Any
任意類型
from typing import Any
a: Any = None
a = [] # OK
a = 2 # OK
TypeVar
TypeVar 可以指定多種類型,或者任意類型
任意類型
from typing import TypeVar, list
T = TypeVar("T") # T 代表了任意類型
x: T = 2
y: T = 'abc'
指定類型
from typing import TypeVar, list
My = TypeVar('My', int, str, list[int]) # 自定義一個類型,可以是 int 或 str 或 list[int] 之一
xx: My = 'ss'
yy: My = [1]
注意這里的
TypeVar()
,后面用的是()
,不是[]
Union
Union 也可以指定多種類型。
from typing import TypeVar, list, Union
x:Union[int, str] = '1' # 可以是 int 或 str 類型
Union 是不分順序的,並且如果有多個重復的類型,會自動去重:
print(
Union[int, str, int, Union[int, list[int]]] == Union[list[int], int, str]
)
# True
這個例子中,重復的
int
會自動去重,並且里面類型的順序也不重要。等式前面的Union中還嵌套了一個Union[int, list[int]]
,也會被提取出來
Python 3.10 版本添加了 |
可以用來簡化 Union 的操作:
x: int | str # 等同於 x: Union[int, str]
|
也可以用於 isinstance, issubclass
等函數中:
isinstance(1, int | str) # True
@overload
有一些函數經常使用 @overload
裝飾器,這其實也是類型提示的一種:
from typing import overload
@overload
def test(a: int): # 僅用來進行參數的類型檢查,說明它支持 int 類型,不用實現函數(即不寫函數內容)
...
@overload
def test(a: str): # 僅用來類型檢查,說明支持 str
...
def test(a): # 很重要,必須寫一個不使用 @overload 的正常的函數。這個函數才是真正被調用的函數。
print(type(a))
test(2)
@overload
裝飾器可以修飾支持多個不同參數類型組合的函數或方法。后續必須緊跟一個非 @overload 裝飾定義的同樣的函數。@overload-裝飾定義僅是為了協助類型檢查器, 因為該裝飾器會被非 @overload-裝飾定義覆蓋
也就是說,你可以寫一堆同名函數,這些函數可以接收不同類型的參數,用 overload
來裝飾這些同名函數,並且最后一定要寫一個正常的、不用overload
裝飾的同名函數。這樣代碼提示就會知道這個函數可以接收哪些類型的參數。