[python] Python型別提示總結

2023-05-10 12:00:25

Python3.5 版本引入了型別提示(Type Hints),它允許開發者在程式碼中顯式地宣告變數、函數、方法等的型別資訊。這種型別宣告不會影響 Python 直譯器的執行,但可以讓 IDE 和靜態分析工具更好地理解程式碼,同時提高程式碼的可讀性和可維護性。然而,由於 Python 支援動態型別,型別提示並不能完全確保程式碼的正確性。本文僅介紹 Python 型別提示的初步使用。如果需要更詳細的使用說明,請參考以下文章:typingPython 型別提示簡介Type Hints 入門教學

型別提示的語法格式為:

  • 對於變數:{變數名}:{型別名} =
  • 對於函數引數:{引數名}:{型別名} =
  • 對於函數返回值:->

1 型別提升

型別提示的引入主要有以下幾個方面的用途:

1 提高程式碼可讀性

型別提示可以幫助其他開發人員更好地理解程式碼,特別是在處理大型程式碼庫時。通過清晰地指定變數、函數引數和返回值的資料型別,開發人員可以更快地理解程式碼的含義和用途,從而更容易維護和修改程式碼。

如下所示。我們有一個名為 add 的函數,用於將兩個數位相加並返回結果。以下是該函數的原始程式碼:

def add(a, b):
    return a + b

我們發現,該函數沒有任何型別提示,因此在呼叫該函數時,我們必須自己去了解和檢查每個引數的型別。這樣會導致程式碼的可讀性和可維護性變差,特別是在程式碼規模較大、涉及多個檔案的情況下。為了改善這種情況,我們可以使用型別提示來明確指定每個引數的型別。以下是新增型別提示後的 add 函數的程式碼:

def add(a: int, b: int) -> int:
    return a + b

現在,我們可以清楚地看到函數 add 的引數和返回值都是整數型別。這使得程式碼更易於理解,也提高了程式碼的可靠性。

2 檢測型別錯誤

Python 是一種動態語言,因此變數和函數引數的型別可以在執行時進行更改。但是,這也意味著開發人員容易在程式碼中引入型別錯誤。通過使用型別提示,開發人員可以在編譯時檢測到這些型別錯誤,並更早地發現和修復它們,從而減少程式碼錯誤和偵錯時間。

mypy是一個用於檢查Python型別的靜態型別檢查器。它可以檢測型別註釋中的錯誤以及其他型別的錯誤。mypy使用說明可以參考:mypy簡易教學。mypy需要首先輸入以下命令安裝:

pip install mypy

然後,在程式碼中標註變數、函數引數和返回值的型別。執行以下命令:

mypy your_script.py

在上面的範例中,your_script.py是要檢查的Python指令碼。執行mypy工具後,它將檢查Python指令碼中的型別錯誤,並輸出錯誤資訊。

3 提供自動補全和檔案

許多整合式開發環境(IDE)和編輯器都可以使用型別提示來提供自動補全和程式碼檔案。這可以幫助開發人員更快地編寫程式碼,並提供關於函數引數和返回值的資訊,以便更好地理解程式碼。要使用Python型別提示提供自動補全和檔案,需要使用一個支援該功能的Python編輯器。比如一些流行的Python編輯器包括vscode、PyCharm和Sublime Text等。

以vscode為例,考慮一個整數相加函數,將結果儲存在變數c中。如果加上型別提示,vscode外掛將推斷變數c的型別為 int,並提供程式碼補全和程式碼提示等功能。

此外,還可以使用vscode的autoDocstring生成帶有型別提示的檔案和註釋。

autoDocstring註釋程式碼使用方法如下所示:

按照以上方法,對於有無型別提示的註釋結果如下:

def add(a, b):
    """_summary_

    Args:
        a (_type_): _description_
        b (_type_): _description_

    Returns:
        _type_: _description_
    """
    c = a + b
    return c


def add(a: int, b: int) -> int:
    """_summary_

    Args:
        a (int): _description_
        b (int): _description_

    Returns:
        int: _description_
    """
    c = a + b
    return c

2 型別宣告

2.1 基本型別

對於Python的內建基本型別 int、float、str 和 byte等,可以直接使用型別本身進行型別提示。如下所示:

# 直接定義
age: int = 1
# 宣告後定義
num: float
num = 2.0

def greet(name: str) -> str:
    return f"Hello, {name}!"

def is_even(x: int) -> bool:
    return x % 2 == 0

def encode_data(data: str) -> bytes:
    return data.encode('utf-8')

2.2 巢狀型別

對於容器資料結構,例如 list、tuple、dict 等,也可以直接使用型別本身進行型別提示。如下所示:

items: list = [1, 4.0, "3"]
info: dict = {"name":"john", "age":24}

在Python的容器資料結構中,每個元素都具有其自己的型別。雖然這種方法提供了靈活性,但是內部元素的型別無法受到限制,因此內部元素可以是任何型別(Any)。可以通過Python的typing標準庫來宣告這些型別及其元素型別。

from typing import List, Tuple, Dict, Set

# 指定my_list變數是一個整數列表
my_list: List[int] = [1, 2, 3, 4]
# 指定my_tuple變數應該是一個按順序包含整數、字串和布林值的元組
my_tuple: Tuple[int, str, bool] = (1, "hello", True)
# 指定了my_dict變數是一個所有鍵為str型別,所有值為int型別的字典
my_dict: Dict[str, int] = {"apple": 1, "banana": 2, "orange": 3}
# 指定了my_set變數應該是一個浮點數集合
my_set: Set[float] = {1.0, 2.0, 3.0}

2.3 自定義型別

Python也支援對自定義類進行型別提示。下面是一個自定義類的型別提示範例:

class Person:
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

def say_hello(person: Person) -> str:
    return f"Hello, {person.name}!"

在上面的程式碼中,我們定義了一個 Person 類,它有兩個屬性:name 和 age。在初始函數中,我們使用型別提示指定了這兩個屬性的型別。接下來,我們定義了一個 say_hello 函數,這個函數的引數是一個 Person 型別的物件,並且返回值是一個字串。

對於numpy和pandas這種第三方庫,也可以通過同樣的方法進行型別提示:

import numpy as np
import pandas as pd
import cv2

# numpy
def add_arrays(a: np.ndarray, b: np.ndarray) -> np.ndarray:
    return np.add(a, b)

# pandas
def filter_dataframe(df: pd.DataFrame, column: str, value: float) -> pd.DataFrame:
    return df[df[column] > value]

# opencv,opencv影象本身就是一個numpy陣列結構
def resize_image(img: np.ndarray, height: int, width: int) -> np.ndarray:
    return cv2.resize(img, (width, height))

2.4 複合型別

2.4.1 Union和Optional

Python的typing庫也提供了Union型別用於表示多種型別中的一種,Optional型別用於表示可選型別。它們可以結合使用,以便更好地表示變數的型別。

例如,如果一個變數可以是整數或字串型別,那麼可以這樣定義它的型別:

from typing import Union

def func(x: Union[int, str]) -> None:
    pass

上面的程式碼中,x的型別為Union[int, str],表示x可以是整數或字串型別。

如果一個變數可以是整數型別或None型別,那麼可以這樣定義它的型別:

from typing import Optional

def func(x: Optional[int] = None) -> None:
    pass

Union和Optional型別可以結合使用。例如,如果一個變數可以是整數型別、字串型別或None型別,那麼可以這樣定義它的型別:

from typing import Optional, Union

def func(x: Optional[Union[int, str]]) -> None:
    pass

上面的程式碼中,x的型別為Optional[Union[int, str]],表示x可以是整數型別、字串型別或None型別。

此外,在Python中,Union[X, Y] 表示變數的型別可以是 X 或 Y。因此,Optional[X] 實際上是 Union[X, None] 的簡寫形式。這種語法的好處是它可以使程式碼更簡潔,因為我們只需要寫一個型別而不是兩個。

from typing import Optional, Union

def greet(name: Optional[str]) -> str:
    if name is None:
        return "Hello, stranger!"
    else:
        return f"Hello, {name}!"

def greet2(name: Union[str, None]) -> str:
    if name is None:
        return "Hello, stranger!"
    else:
        return f"Hello, {name}!"

在上面程式碼中,greet和greets函數是等價的。在第一個函數中,我們使用了 Optional[str] 來表示 name 可以是一個字串或者是 None。在第二個函數中,我們使用了 Union[str, None] 來達到相同的效果。

2.4.2 Generator和Iterator

在Python中,Generator和Iterator是非常常見的資料型別。Generator是一種函數,可以通過yield語句生成一個迭代器,而Iterator是一種物件,可以用於迭代元素序列。為了提高程式碼的可讀性和可維護性,我們可以使用型別提示來指定Generator和Iterator的型別。

Generator型別提示使用Generator[ReturnType, SendType, ReturnType]語法,其中ReturnType指定返回值型別,SendType指定傳送值型別,ReturnType指定生成器的型別。例如,下面是一個簡單的Generator型別提示範例:

from typing import Generator

def even_numbers(n: int) -> Generator[int, None, None]:
    for i in range(n):
        if i % 2 == 0:
            yield i

上面的程式碼中,even_numbers是一個Generator函數,返回型別是Generator[int, None, None],該函數生成一個整數序列,其中每個偶數都是通過yield語句生成的。

Iterator型別提示使用Iterator[ElementType]語法,其中ElementType指定迭代器元素型別。例如,下面是一個簡單的Iterator型別提示範例:

from typing import Iterator

class MyIterator:
    def __init__(self):
        self.current: int = 0
        self.max: int = 5

    def __iter__(self) -> Iterator[int]:
        return self

    def __next__(self) -> int:
        if self.current >= self.max:
            raise StopIteration
        else:
            self.current += 1
            return self.current

在上面的程式碼中,我們對MyIterator類進行了註釋。使用了typing模組中的Iterator類來註釋__iter__()方法的返回值型別。同時,我們對current和max屬性也進行了註釋,指定了它們的型別為int。在__next__()方法中,我們指定了返回值型別為int。

2.4.3 Callable

Callable型別提示用於表示一個可呼叫物件,例如函數、類或物件等。從形式上來看,Callable型別提示接受兩個或三個型別提示引數:第一個參數列示函數的引數型別,第二個參數列示函數的返回型別。下面是一個Callable型別提示的例子:

from typing import Callable

def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
    return func(a, b)

def add(a: int, b: int) -> int:
    return a + b

result = apply(add, 3, 4)
print(result) # 輸出7

在上面的例子中,apply函數接受一個名為func的引數,該引數是一個Callable型別,它指定了函數的兩個整數引數和一個整數返回值。add函數滿足這個條件,因此可以傳遞給apply函數,它會返回add(3, 4)的結果7。

2.4.4 Any和NoReturn

Any型別表示一個任意型別,它可以用於函數引數、函數返回值和變數等。使用Any型別時,我們可以省略型別註釋,使變數型別更加靈活。下面是一個使用Any型別的例子:

from typing import Any

def print_value(value: Any) -> None:
    print(value)

print_value("Hello World")  # 輸出 "Hello World"
print_value(123)           # 輸出 123

在上面的例子中,我們定義了一個print_value函數,它接受一個任意型別的引數value,並將其列印出來。我們可以看到,我們可以將任何型別的值傳遞給print_value函數,包括字串和整數。這使得我們的程式碼更加靈活。

NoReturn型別表示函數不會返回任何值。這個型別通常用於標識那些沒有返回值的函數。下面是一個使用NoReturn型別的例子:

from typing import NoReturn

def print_message(message: str) -> NoReturn:
    print(message)
    raise Exception("Error occurred")

print_message("Hello World")  # 輸出 "Hello World",然後丟擲異常

在上面的例子中,我們定義了一個print_message函數,它接受一個字串型別的引數message,並將其列印出來。然後,我們手動丟擲了一個異常,這意味著函數不會返回任何值。我們可以使用NoReturn型別來明確地表示這一點。

2.4.5 其他

Python還支援更高階的型別提示。例如,可以使用Sequence來指定一個列表,使用TypedDict來指定一個帶有特定鍵和值型別的字典。此外,Python還支援Literal型別提示,可以限制變數只能取特定的常數值。最近,Python3.8版本還增加了Protocol型別提示,允許指定類需要實現哪些方法和屬性。這些型別提示用的不多,但是如果需要更精細的型別控制,可以參考官方檔案:typing

2.5 型別提示的別名

在型別提示中使用了過於複雜的型別,可以考慮將其定義為一個型別別名,然後在函數引數、返回值等處使用該型別別名。例如,如果你需要傳遞一個包含多個欄位的字典作為函數引數,你可以使用Dict[str, Union[int, str, List[int]]]來表示該字典的型別。但是,這個型別過於複雜,不易於理解。你可以將其定義為一個型別別名,如下所示:

from typing import Dict, Union, List

MyDict = Dict[str, Union[int, str, List[int]]]

def my_function(my_dict: MyDict) -> int:
    # Function body
    return 1

這樣,你就可以在函數引數、返回值等處使用MyDict這個型別別名,使程式碼更加易讀、易懂。

3 參考