物件導向 <<
Previous Next >> 開發
類型註解
基於協同開發,Python 引入了選擇性的類型註解。
學員可以藉由類型註解,在程式碼協同時較快辨認變數類型。
類型註解
類型註解 (Typing) 是一種註解,可以為每種參數進行標示。由於文法問題,類型註解並不是強迫性的,不過仍有效用。可藉由每個範圍 (Scope) 的 __annotations__ 名稱取得,若有工具或 IDE 的功能,可以進行靜態分析。
以下內容依據為 PEP 484。
省略記號
省略記號 (Ellipsis)「...」在 Python 中是一種佔位符號,可以在註解中代表多個類似物件,也能取代 pass 關鍵字。
等價表示
預設該值如果為 Any 或可以明確推斷,可以選擇不標示。
其中:
- type(None) 標示為 None。
- object 標示為 Any。
- 會導致錯誤時標示為 NoReturn,僅可用於回傳值。
基本文法
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) 的規定,「:」符號會連接前一個表示式,與下一個表示式間隔空白;「->」記號則是必須由空白環繞。
# 只能從預設值或參數名稱猜測。
def func(p0, p1=20, p2=True):
...
# 直接規定型別。
def func(p0: int, p1: int = 20, p2: bool = True) -> List[str]:
...
# 若是太長可以利用括弧換行。
def func(
number: int,
size: int = 20,
reverse: bool = True
) -> List[str]:
...
在 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 (
# 狀態變數
TYPE_CHECKING,
# 序列
Tuple,
List,
Secquence,
# 二元搜尋樹
Set,
Dict,
# 可呼叫物(函式)
Callable,
# 迭代器與生產器
Iterator,
Generator,
# 邏輯判斷
Optional,
Union,
Any,
# 泛型
Generic,
# 函式或裝飾器
TypeVar,
overload,
)
以下將介紹上述常用的類型標示。
容器
使用單一項目的容器:
# 其實 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 = []
...
泛型
自訂類型中有客製化的類型選擇時,就可以用泛型標示。
例如:
from abc import abstractmethod
from typing import Iterator, Generic, TypeVar
_T = TypeVar('_T')
class BaseTable(OtherTable, Generic[_T]):
@abstractmethod
def data(self, row: int) -> _T:
...
def data_iter(self) -> Iterator[_T]:
for row in range(self.row_count()):
yield self.data(row)
class MyTable(BaseTable[float]):
def data(self, row: int) -> float:
"""實作此方法。"""
...
上述的 MyTable 類型的 data_iter 方法就會回傳 Iterator[float] 類型了。
遞迴引用
遞迴引用類型註解時,直接使用字串即可:
Node: Dict[str, Optional[List['Node']]]
在類型定義中引用自己:
class MyClass:
def return_me(self, friend: 'MyClass') -> 'MyClass':
"""實例 self 可能是子類型,因此不會標示。"""
...
無法導入的名稱或模組:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core import MyClass
class LocalClass:
def __init__(self, parent: 'MyClass'):
"""想要取得父項的屬性。"""
self.my_list = parent.parent_list
...
而在 Python 3.7 中,引入了新的延遲分析行為 (PEP 563: Postponed Evaluation of Annotations),將會強制將類型註解轉為字串再分析,並且這將是 Python 4 的預設作為。不過此行為牽涉到文法問題,因此使用向下相容模組 __future__ 來開啟。
這樣一來,在「舊版」的 Python 中就可以使用遞迴引用了。
from __future__ import annotations
class MyClass:
def return_me(self, friend: MyClass) -> MyClass:
...
物件導向 <<
Previous Next >> 開發