Python 類型註解

基於協同開發,Python 引入了選擇性的類型註解。

建議先瞭解:

  • 物件導向概念
  • 簽章
  • 自行閱讀 Python 的撰寫風格規定 PEP 8

學員可以藉由類型註解,在程式碼協同時較快辨認變數類型。

類型註解

類型註解 (Typing) 是一種註解,可以為每種參數進行標示。由於文法問題,類型註解並不是強迫性的,不過仍有效用。可藉由每個範圍 (Scope) 的 __annotations__ 名稱取得,若有工具或 IDE 的功能,可以進行靜態分析。

根據 PEP 484 的內容,類型註解物件可以自行製作和定義,但是本節只會將常用容器列出。

省略記號

省略記號 (Ellipsis) ... 在 Python 中是一種佔位符號,可以在註解中代表多個類似物件,甚至也能取代 pass 關鍵字。

基本文法

Python 為弱型別 (Weak typing) 語言,又稱鴨子型別 (Duck type),相較於強型別 (Strong typing) 語言,只要變數能用即可,不能用就引發錯誤。當然這只能在相對安全的直譯式語言 (Interpreted language) 中,因為這裡只要使用 try 語句即可:

try:
    # 測試 a 能不能執行 test_method。
    b = a.test_method()
except AttributeError:
    # 沒有 test_method。
    c = 20
else:
    c = b * 2

但是因為執行效能與開發效率問題,一直測試顯然不是好方法,因此 Python 在簽章上引入了類型註解的文法。

簽章的參數中,使用 : 符號後連接類型名稱;回傳值則是使用 -> 記號連接。參照於一般英文符號以及運算子必須由空白環繞 (PEP 8) 的規定,: 符號會連接前一個表示式,與下一個表示式間隔空白;-> 記號則是必須由空白環繞。

回傳值如果為 None,可以選擇不標示。

# 只能從預設值或參數名稱猜測。
def func(p0, p1=20, p2=True):
    pass

# 直接規定型別。
def func(p0: int, p1: int = 20, p2: bool = True) -> List[str]:
    pass

# 若是太長可以利用括弧換行。
def func(
    number: int,
    size: int = 20,
    reverse: bool = True
) -> List[str]:
    pass

在 Python 3.6 新增單一變數的類型註解文法 (PEP 526)。

不過一般可直接辨識的變數就不會使用,例如直接賦予類型的初始化物件。

# 等等會裝入整數。
a: List[int] = []
# 這麼明顯就不用了。
a: MyClass = MyClass()

類型註解也支援名稱替換:

Point = Tuple[float, float]
PointPair = Tuple[Point, Point]
graphics: Dict[str, List[PointPair]] = {}

參數輸入值的類型註解一般也是用鴨子型別的概念標示,提醒開發者「需要這樣使用」,而非「一定需要這種類型」。如:

def func(w) -> bool:
    """w 是一個會進行疊代與檢索的物件。"""
    for i in range(20):
        for k in w:
            if w[i + 2] == 'z':
                return True

# 應該標示成序列:
w: Secquence[int]
# 而非強迫成某種型態:
w: List[int]
w: Tuple[int, ...]

自 Python 3.5 起支援,支援放入回傳的型別必須由標準模組 typing 提供。類型中的類型註解名稱會跟一般內建類型名稱不一樣,改成字首大寫。

通常 typing 模組的導入習慣將相同性質的類型一起擺放。

from typing import (
    # 序列
    Tuple,
    List,
    Secquence,
    # 二元搜尋樹
    Set,
    Dict,
    # 可呼叫物(函式)
    Callable,
    # 迭代器與生產器
    Iterator,
    Generator,
    # 邏輯判斷
    Optional,
    Union,
    Any,
)

以下將介紹上述常用的類型標示。

容器

使用單一項目的容器:

# 其實 tuple 容器是固定長度的。
my_tuple: Tuple[int, int] = (20, 20)
# 不限長度的容器。
my_list: List[float] = [20., 50.02, -3.006]
# 不限長度的 tuple 容器(其他變數決定)。
my_tuple: Tuple[int, ...] = tuple(i for i in range(s))
# 巢狀標示。
w: Set[Tuple[int, int]] = {(10, 20), (30, 40), (50, 60)}

使用成對項目的容器:

d: Dict[str, List[int]] = {
    's': [10, 20, 30],
    'b': [],
    'f': [77, 66, 55, 44],
}

函式物件

函式物件使用 Callable 來標示:

Callable[[input_type, ...], return_type]

範例:

def func(n: int) -> int:
    """一般函式"""
    return n * 6

# 匿名函式
k = lambda s, end: s.replace('gen', 'time') + end

func: Callable[[int], int]
k: Callable[[str, str], str]

迭代器與生產器

使用 yield 關鍵字可以搭配 def 關鍵字定義一個迭代器 (Iterator) 或生產器 (Generator) 函式:

def double_range(n):
    """這個 double_range 是一個函式
    但是可以產生以下程式碼的迭帶器物件。

    若當中使用 return 關鍵字,會引發 StopIteration 錯誤。
    不過也可以是無限迴圈。
    """
    for i in range(n):
        yield i * 2

# 建立迭帶器物件 "ten_double_range"。
ten_double_range = double_range(10)
# 使用用內建函式 next 可以產生下一個值。
print(next(ten_double_range))  # 0
print(next(ten_double_range))  # 2
print(next(ten_double_range))  # 4
# 或是使用 for 迴圈連續取值,
# 直到引發 StopIteration 錯誤(不會引發實際 Error)。
for factor in ten_double_range:
    print(factor)  # 6 8 10 12 14 16 18 20
# 當取完值後,再次呼叫會引發 StopIteration 錯誤。
# 此時迭帶器物件無法再使用,必須丟棄。
# next(ten_double_range)

生產器範例:

def double_inputs():
    """這個 double_inputs 是一個函式
    但是可以產生以下程式碼的生產器物件。

    生產器可以接收值。
    """
    while True:
         # 當 yield 擺在右值時可以接收值。
         x = yield
         # 當 yield 右邊有值時可以產生值。
         yield x * 2
         # 不使用名稱可以這樣寫。
         yield (yield) * 2

gen = double_inputs()
next(gen)  # 跳至第一個 yield,不過會回傳 None。
print(gen.send(10))  # 輸入 10,回傳 20。
next(gen)  # 跳至下一個 yield,不過會回傳 None。
print(gen.send(6))  # 輸入 6,回傳 12。

類型標註如下:

Iterator[yield_type]
# Python 3.6 增加
Generator[yield_type, input_type, return_type]

上述製造迭代器與生產器的函式應標註為:

def double_range(n: int) -> Iterator[int]:
    ...

def double_inputs() -> Generator[int, int, None]:
    ...

ten_double_range: Iterator[int]
gen: Generator[int, int, None]

邏輯判斷

類型註解包含被繼承類型,因此其實每個物件都是 object 類型。

若是沒有繼承關係,但是可以進行相同操作,因此 typing 模組提供方便的邏輯標示。

聯集 (Union) 類型能夠代表多個不同的類型:

Union[T1, T2, ...]

選擇性 (Optional) 類型能夠代表該類型可能會為 None

Optional[T]
# 同於 Union
Union[T, None]

可以用在簽章的預設值:

def func(w: List[int] = []):
    """這樣會導致預設值的指標被共用。"""
    ...

def func(w: Optional[List[int]] = None):
    """這樣就不會共用。"""
    if w is None:
        w = []
    ...

任意 (Any) 類型能夠代表任何 Python 物件,同於 object

# 例如從 json 檔案解析的檔案結構可能有任何數值。
Dict[str, Any]

遞迴引用

遞迴引用類型註解時,直接使用字串即可:

Node: Dict[str, List['Node']]
Tree: Dict[str, List[Node]]

在類型定義中引用自己:

class MyClass:
    def return_me(self, friend: 'MyClass') -> 'MyClass':
        """實例 self 可能是子類型,因此不會標示。"""
        ...

無法導入的名稱或模組:

# 因為 core.module_top.kernel 引用此模組了,
# 所以不能從該模組中使用其中名稱。
from core.module_top import kernel as kn


class LocalClass:
    def __init__(self, parent: 'kn.MyClass'):
        """想要取得父項的屬性。"""
        self.my_list = parent.parent_list
        ...
Show Comments