python怎么做類型標(biāo)注

盡管pyhton從3.5版本開始就引入了類型系統(tǒng),但到目前為止,接受程度不是特別的高,很多的開源庫仍然沒有使用類型標(biāo)注。
究其原因,我認(rèn)為最主要的一條是類型標(biāo)注不是必須的,且python解釋器并不檢查你所做的類型標(biāo)注,那么大家在代碼里添加類型標(biāo)注的動力也就不大。
一項(xiàng)好的技術(shù),沒有被推廣,單純的說它不是必須的,不能解釋所有的問題,另一個影響類型標(biāo)注廣泛使用的原因,我認(rèn)為是類型標(biāo)注有一點(diǎn)難度,官方文檔沒有盡全力為大家解釋該如何使用。這使得很多想要使用類型標(biāo)注的人望而卻步,畢竟,即便費(fèi)力的做了,也沒有很明顯很直觀的收益。
本文將通過實(shí)際的例子為你展示如何在python代碼里做類型標(biāo)注。
1. 為變量做類型標(biāo)注
我們先來通過最簡單的情況,為變量做類型標(biāo)注,變量可以有如下類型:
int
float
bool
str
bytes
None
list
tuple
set
dict
1.1 簡單的數(shù)據(jù)類型
int, float, bool, str, None,bytes 這些都是最簡單的數(shù)據(jù)類型,他們的類型標(biāo)注也是最簡單的,創(chuàng)建腳本typehint.py
from typing import Optional, Union, Any
a: int = 8
b: bool = True
c: str = 'ok'
d: None = None
e: float = 9.8
f: bytes = b'32'
使用mypy對類型標(biāo)注進(jìn)行檢查
mypy typehint.py
檢查結(jié)果
Success: no issues found in 1 source file
這說明我們對這4種變量的類型標(biāo)注是正確的,但上面的代碼存在嚴(yán)重的缺陷,變量d我為它標(biāo)注為None,那么d這個變量就永遠(yuǎn)只能為None了,如果我將其賦值為其他類型,類型標(biāo)注檢查就會報錯, 修改代碼
from typing import Optional, Union, Any
a: int = 8
b: bool = True
c: str = 'ok'
d: None = None
e: float = 9.8
f: bytes = b'32'
d = 5
使用mypy檢查結(jié)果
typehint.py:9: error: Incompatible types in assignment (expression has type "int", variable has typ
e "None")
Found 1 error in 1 file (checked 1 source file)
修改后的代碼可以正常執(zhí)行,因?yàn)閜ython解釋器才不管類型標(biāo)注呢,但是將5賦值給d就不符合類型標(biāo)注的要求了。
類型標(biāo)注的意義是標(biāo)注一個變量的數(shù)據(jù)類型,此后的代碼都應(yīng)當(dāng)遵守對這個變量的類型標(biāo)注,這就要求我們,不能隨意的修改變量的數(shù)據(jù)類型。
1.2 使用 Optional
在1.1 的例子中,d變量別標(biāo)注為None類型,可一個變量始終賦值為None是毫無意義的事情,你只是在最初的時候不想給它一個明確的值才賦值為None的,后面的代碼一定會修改變量d的值的。
假設(shè)你對變量d的使用是希望為它賦值一個int類型的數(shù)據(jù),那么在類型標(biāo)注的時候,就應(yīng)當(dāng)做好準(zhǔn)備
from typing import Optional, Union, Any
a: int = 8
b: bool = True
c: str = 'ok'
d: Optional[int] = None
e: float = 9.8
f: bytes = b'32'
d = 5
Optional表示可選,那么d就可以被賦值成int類型,此外也可以是None。
1.3 使用Union
d 能賦值成int,也可能被賦值成float, 這種情況,要結(jié)合Optional 和 Union
from typing import Optional, Union, Any
a: int = 8
b: bool = True
c: str = 'ok'
d: Optional[Union[int, float]] = None
e: float = 9.8
f: bytes = b'32'
d = 5
d = 9.8
d = None
Union表示或的意思,d變量的類型,可以是None,也可以是int或者float。
接下來,你可能會問,可不可以將a變量的類型標(biāo)注設(shè)置為Union[int, float], 讓a以賦值成int也可以賦值成為float?從純粹的技術(shù)實(shí)現(xiàn)上講這樣做沒有問題
from typing import Optional, Union, Any
a: Union[int, float] = 8 # 堅(jiān)決反對你這樣做
b: bool = True
c: str = 'ok'
d: Optional[Union[int, float]] = None
e: float = 9.8
f: bytes = b'32'
d = 5
d = 9.8
d = None
a = 8.9
但從工程實(shí)踐的角度來看,這種做法簡直就是脫褲子放屁,多此一舉。我們?yōu)樽兞窟M(jìn)行類型標(biāo)注的目的就是為了防止變量在使用過程中由于缺乏類型檢查導(dǎo)致類型變來變?nèi)ィ氵@樣不就是又回到了之前的狀態(tài)了么,那做類型標(biāo)注還有什么意義呢,還不如不做。
d變量與其他幾個變量不同,d變量初始值賦值為None,我們心里很清楚,它的值一定會被改變的,不然留著它毫無意義, 而一旦改變,就必然導(dǎo)致數(shù)據(jù)類型發(fā)生變化,因此才需要我們使用Optional。其他變量呢,值改變了,數(shù)據(jù)類型可以不發(fā)生變化,如果類型發(fā)生了變化,說明你的操作就違背了類型標(biāo)注的初衷。
1.4 為容器類型做標(biāo)注
list, tuple, dict, set, 為這4個容器類型數(shù)據(jù)做標(biāo)注,要稍微麻煩一點(diǎn)點(diǎn), 先來看最簡單的set,
1.4.1 為集合做標(biāo)注
在使用set時,我們默認(rèn)只會向集合中添加相同數(shù)據(jù)類型的值,但你要明確一點(diǎn),集合可以存儲不同類型的數(shù)據(jù)。
from typing import Optional, Union, Any, Set
s: Set[int] = {1, 2, 3}
這段代碼可以通過mypy的檢查, 接下來看列表如何做標(biāo)注
1.4.2 為列表做標(biāo)注
from typing import Optional, Union, Any, Set, List, Tuple
s: Set[int] = {1, 2, 3}
l: List[int] = [1, 2, 3]
列表標(biāo)注的方式與集合是一樣的,但我們都清楚,列表里存儲的數(shù)據(jù)往往都是類型不相同的,比如下面的列表
[1, 2, 3, 'a', 'b', True]
對這種情況,就需要使用1.3小節(jié)所介紹的Union
from typing import Optional, Union, Any, Set, List, Tuple
s: Set[int] = {1, 2, 3}
l: List[Union[int, str, bool]] = [1, 2, 3, 'a', 'b', True]
1.4.3 為元組做標(biāo)注
為元組做標(biāo)注,不能使用和列表相同的辦法,而是要逐個索引位置進(jìn)行標(biāo)注
from typing import Optional, Union, Any, Set, List, Tuple
t: Tuple[int, str, bool] = (3, 'ok', True)
1.4.4 為字典做標(biāo)注
先來看最簡單的,字典的key都是字符串,value都是int
from typing import Optional, Union, Any, Set, List, Tuple, Dict
d: Dict[str, int] = {'ok': 4}
這是最理想的情況,但實(shí)際情況往往更復(fù)雜,字典的key可以有str類型,也可以有int類型,當(dāng)類型不確定的時候,我們就可以使用Union
from typing import Optional, Union, Any, Set, List, Tuple, Dict
d: Dict[str, int] = {'ok': 4}
d1: Dict[Union[str, int], Union[str, int, float]] = {'ok': 4, 3: 'ok', 4: 3.2}
還有更復(fù)雜的情況
from typing import Optional, Union, Any, Set, List, Tuple, Dict
dic: Dict[str, Union[Tuple[int, int], Dict[int, int]]] = {
'ok': (1, 2),
'dic': {5: 9}
}
字典里的value,可以是元組,也可以是字典,字典嵌套了字典,在做類型標(biāo)注的時候,也就需要以嵌套的形式進(jìn)行標(biāo)注。對于這種復(fù)雜的字典,我的建議就是簡化處理
from typing import Optional, Union, Any, Set, List, Tuple, Dict
dic: Dict[str, Union[Tuple, Dict]] = {
'ok': (1, 2),
'dic': {5: 9}
}
value可以是元組,也可以是字典,我只要標(biāo)注到這個程度就可以了,不再繼續(xù)詳細(xì)的進(jìn)行標(biāo)注,不然單單一個類型標(biāo)注就把代碼搞的難以理解了。
1.4.5 容器類型標(biāo)注總結(jié)
容器類型標(biāo)注,可以粗略的進(jìn)行標(biāo)注,也可以詳細(xì)的進(jìn)行標(biāo)注,這完全取決于你的想法,我的觀點(diǎn)是,在不影響代碼可閱讀性的前提下詳細(xì)標(biāo)注,反之則粗略標(biāo)注
from typing import Optional, Union, Any, Set, List, Tuple, Dict
l: List = [1, 2, ['2', '3']] # 粗略標(biāo)注
l2 : List[Union[int, List[str]]] = [1, 2, ['2', '3']] # 詳細(xì)標(biāo)注
2. 為函數(shù)做標(biāo)注類型
2.1 對形參和返回值進(jìn)行標(biāo)注
為函數(shù)做標(biāo)注類型,需要對每一個形參做類型標(biāo)注,同時還要對函數(shù)的返回值做類型標(biāo)注
def add(x: int, y: int) -> int:
return x + y
print(add(2, 5))
形參的變量類型,我們事先是清楚的,因此你只需要按照第一節(jié)里的講解對形參進(jìn)行標(biāo)注就可以了,函數(shù)的返回值在函數(shù)定義時進(jìn)行標(biāo)注,在有括號后面緊跟著進(jìn)行標(biāo)注,注意需要用到“->”。
如果返回值的類型可能是int,也可能是None,該怎么標(biāo)注呢?其實(shí)這種情況完全可以參考對變量的標(biāo)注
from typing import Optional
def add(x: Optional[int], y: int) -> Optional[int]:
if not isinstance(x, int):
return None
return x + y
add(3, 4)
add(None, 4)
看到這里你應(yīng)該明白,對函數(shù)參數(shù)及返回值的標(biāo)注,完全遵守對變量的標(biāo)注規(guī)則,唯一需要區(qū)別對待的是函數(shù)的返回值。
2.2 對可變參數(shù)進(jìn)行標(biāo)注
python的可變參數(shù)一個是*args, 一個是**kwargs,從函數(shù)的視角來看,args的類型是元組,kwargs的類型是字典,先來看args
def add(*args: int) ->int:
sum_value = sum(args)
return sum_value
print(add(1, 2, 3))
我很確定args里的元素都是int類型,那么直接標(biāo)注為int就可以了,如果還有其他類型,那么就需要使用Union
from typing import Optional, Union
def add(*args: Union[str, int, float]) -> float:
sum_value = sum([float(item) for item in args])
return sum_value
print(add(1, '2', 3.8))
傳入的可變參數(shù)可以是str,int,float中的任意一個,args雖然是元組,但是我們不是按照元組來進(jìn)行標(biāo)注,標(biāo)注的是對這些參數(shù)的期望值,再來看**kwargs
from typing import Any, Union
def add(**kwargs: Union[int, str, float]) -> None:
print(kwargs)
dic = {
'a': 3,
'b': '5',
'c': 9.3
}
add(**dic)
add(a=3, b='5', c=9.3)
關(guān)鍵字參數(shù)的值,有int,str,float三個類型,我們要標(biāo)注的是這些參數(shù)的值,而不是字典。
2.3 callable對象做參數(shù)
在python中,函數(shù)也是對象,也可以作為函數(shù)的參數(shù)
from typing import Callable, Any, Union
import time
from functools import wraps
def cost(func: Callable):
@wraps(func)
def warpper(*args: Any, **kwargs: Any):
t1 = time.time()
res = func(*args, **kwargs)
t2 = time.time()
print(func.__name__ + "執(zhí)行耗時" + str(t2-t1))
return res
return warpper
@cost
def test(sleep_time: Union[float, int]) -> None:
"""
測試裝飾器
:param sleep_time:
:return:
"""
time.sleep(sleep_time)
test(1)
當(dāng)形參是函數(shù)對象時,使用Callable進(jìn)行標(biāo)注。
3. 標(biāo)注自定義類
3.1 自定義類實(shí)例
在程序里自定義了一個類,對于這個類的實(shí)例,我們也可以標(biāo)注
class Demo():
pass
d : Demo = Demo()
def test(demo: Demo):
pass
test(d)
3.2 標(biāo)注類屬性
類屬性可以使用ClassVar進(jìn)行標(biāo)注,標(biāo)注后,如果實(shí)例嘗試修改類屬性,mypy在檢查時會報錯,但python解釋器可以正常執(zhí)行程序,原因前面已經(jīng)強(qiáng)調(diào)過,解釋器不受類型標(biāo)注影響
from typing import ClassVar
class Demo():
count: ClassVar[int] = 0
d: Demo = Demo()
print(d.count)
d.count = 20 # mypy 檢查會報錯
4. 不常見的類型標(biāo)注
有些對象的類型不如基礎(chǔ)數(shù)據(jù)類型那樣常見,我這里做一個總結(jié)并一一舉例說明
4.1 迭代器
from typing import Iterator
def my_generator(n: int) -> Iterator:
index = 0
while index < n:
yield index
index += 1
generate = my_generator(5)
my_generator是生成器函數(shù),它的返回值是一個generator類型的對象,是一個迭代器,的返回值就可以標(biāo)注為Iterator
4.2 字典的items(), keys(), values()返回值
字典的items(),keys(),values()三個方法分別返回字典的key-value對,所有的key和所有的values,標(biāo)記他們類型的方法如下:
from typing import ItemsView, KeysView, ValuesView
def test_1() -> ItemsView:
dic = {'name': 'python'}
return dic.items()
def test_2() ->KeysView:
dic = {'name': 'python'}
return dic.keys()
def test_3() ->ValuesView:
dic = {'name': 'python'}
return dic.values()
4.3 Sequence
Sequence 可以用來標(biāo)記任何序列對象,比如列表,元素,字符串,字節(jié)串,他們都是序列,如果你對變量的類型不是很確定,但可以肯定它一定是一個序列,那么就可以使用Sequence
from typing import Sequence, List
lst: Sequence[int] = []
name: Sequence = 'python'
tup: Sequence = (1, 2, 4.5)
bstring: Sequence = b'sds'
5. 泛型和TypeVar工廠函數(shù)
泛型和TypeVar工廠函數(shù),都是為了更方便的進(jìn)行類型標(biāo)注而存在的。假設(shè)你現(xiàn)在要定義一個棧,Stack類,你需要一個列表來存儲數(shù)據(jù),此時,你會遇到一個難處,如果這個棧只允許int類型數(shù)據(jù)入棧,那么你就只能這樣定義
from typing import List
class Stack():
def __init__(self):
self.data: List[int] = []
但如果這個棧只允許float類型的數(shù)據(jù)入棧,你就只能這樣來定義
class Stack():
def __init__(self):
self.data: List[float] = []
這樣就有點(diǎn)犯難了,兩個存儲不同數(shù)據(jù)類型的棧就需要兩個定義,但這兩個類的代碼是完全一致的,只是類型標(biāo)注不同,有沒有什么辦法,可以用一套代碼實(shí)現(xiàn)不同類型的標(biāo)注呢?
這就要用到泛型和TypeVar函數(shù)
from typing import TypeVar, Generic, List
T = TypeVar('T')
class Stack(Generic[T]):
def __init__(self):
self.data: List[T] = []
def push(self, item: T):
self.data.append(item)
def pop(self) -> T:
return self.data.pop(-1)
def top(self) -> T:
return self.data[-1]
def size(self) -> int:
return len(self.data)
def is_empty(self) -> bool:
return len(self.data) == 0
stack = Stack[int]()
stack.push(3)
stack.push(5)
print(stack.pop())
我定義一個泛型,所謂泛型,就是先不明確它的類型,那么什么時候明確它的類型呢,等到實(shí)際調(diào)用的時候,比如
stack = Stack[int]()
我在創(chuàng)建stack對象時來確定泛型T的數(shù)據(jù)類型,如果你希望棧只存儲float類型數(shù)據(jù),你就可以這樣來寫
stack = Stack[float]()
使用泛型,相當(dāng)于創(chuàng)建了一個模板,在調(diào)用模板前,來確定泛型的數(shù)據(jù)類型,一套代碼,實(shí)現(xiàn)了多套數(shù)據(jù)類型標(biāo)注,豈不美哉。
