Python 型別註解

奧森iorson發表於2023-02-12

在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)像靜態語言那樣分析我們的程式碼,及時給我們相應的提示,如下圖對比:

image-20230212140705227
VS
image-20230212140817448

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,
}
image-20230212162843194

可以看出,pycharm也會警告我們字典例項中缺失的key。

同時,在我們生成字典例項的時候,pycharm也會給我們key的提示。

image-20230212163042746

型別別名

型別別名是透過將型別分配給別名來定義的,型別別名可用於簡化複雜型別提示。

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")
image-20230212162309699

可以看出,pycharm檢查出了我們輸入的值並不符合字面量規定的值,進而出現了黃色警告。

image-20230212163929671

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

"""
透過告警,我們提前發現了混用型別的問題,避免了程式執行時發生異常的可能。
"""

泛型很巧妙地對型別進行了引數化,同時又保留了函式處理不同型別時的靈活性。


引用

1、Python 標準庫 typing 型別註解標註

2、Python型別註解,你需要知道的都在這裡了

相關文章