入門系列介紹了 TypeVar 的基本用法。本章深入探討泛型的進階特性,讓你能夠建立型別安全的抽象層。

先備知識

TypeVar 進階

bound 參數

bound 限制 TypeVar 必須是某個型別的子型別:

 1from typing import TypeVar
 2
 3class Animal:
 4    def speak(self) -> str:
 5        return "..."
 6
 7class Dog(Animal):
 8    def speak(self) -> str:
 9        return "Woof!"
10
11class Cat(Animal):
12    def speak(self) -> str:
13        return "Meow!"
14
15# T 必須是 Animal 或其子類別
16T = TypeVar("T", bound=Animal)
17
18def make_speak(animal: T) -> str:
19    return animal.speak()  # 型別檢查器知道 animal 有 speak() 方法
20
21# 正確使用
22make_speak(Dog())  # OK
23make_speak(Cat())  # OK
24
25# 錯誤使用
26make_speak("not an animal")  # 型別錯誤

bound vs 限制型別

1from typing import TypeVar
2
3# 方式一:bound - T 可以是 Animal 或任何子類別
4T_bound = TypeVar("T_bound", bound=Animal)
5
6# 方式二:限制型別 - T 只能是 Dog 或 Cat,不能是其他 Animal 子類別
7T_constrained = TypeVar("T_constrained", Dog, Cat)

差異:

方式適用情況
bound=AnimalT 可以是 Animal 或任何子類別(包括未來新增的)
TypeVar("T", Dog, Cat)T 只能是明確列出的型別

covariant 與 contravariant

這是泛型最難理解的概念,但對於設計型別安全的 API 非常重要。

問題情境

 1class Animal:
 2    pass
 3
 4class Dog(Animal):
 5    pass
 6
 7# 問題:List[Dog] 是 List[Animal] 的子型別嗎?
 8def process_animals(animals: list[Animal]) -> None:
 9    animals.append(Animal())  # 如果傳入 list[Dog],這會破壞型別安全
10
11dogs: list[Dog] = [Dog(), Dog()]
12process_animals(dogs)  # 如果允許,dogs 裡面會有 Animal!

Python 的 list不變的(invariant),所以 list[Dog] 不是 list[Animal] 的子型別。

covariant(協變)

只讀的容器可以是協變的:

 1from typing import TypeVar, Generic, Iterator
 2
 3T_co = TypeVar("T_co", covariant=True)
 4
 5class ReadOnlyBox(Generic[T_co]):
 6    """只能讀取,不能修改"""
 7    def __init__(self, value: T_co) -> None:
 8        self._value = value
 9
10    def get(self) -> T_co:
11        return self._value
12
13# ReadOnlyBox[Dog] 是 ReadOnlyBox[Animal] 的子型別
14def show_animal(box: ReadOnlyBox[Animal]) -> None:
15    print(box.get())
16
17dog_box: ReadOnlyBox[Dog] = ReadOnlyBox(Dog())
18show_animal(dog_box)  # OK - Dog 是 Animal

記憶方式:協變 = 輸出方向,子型別可以替代父型別。

contravariant(逆變)

只寫的容器可以是逆變的:

 1from typing import TypeVar, Generic
 2
 3T_contra = TypeVar("T_contra", contravariant=True)
 4
 5class Handler(Generic[T_contra]):
 6    """處理器:只接收值"""
 7    def handle(self, value: T_contra) -> None:
 8        print(f"Handling: {value}")
 9
10# Handler[Animal] 是 Handler[Dog] 的子型別!(反直覺)
11def setup_dog_handler(handler: Handler[Dog]) -> None:
12    handler.handle(Dog())
13
14animal_handler: Handler[Animal] = Handler()
15setup_dog_handler(animal_handler)  # OK - 能處理 Animal 就能處理 Dog

記憶方式:逆變 = 輸入方向,父型別可以替代子型別。

實際應用

 1from typing import Callable
 2
 3# Callable 的參數是逆變的,返回值是協變的
 4# Callable[[Animal], Dog] 是 Callable[[Dog], Animal] 的子型別
 5
 6def process(func: Callable[[Dog], Animal]) -> None:
 7    result = func(Dog())
 8    print(result)
 9
10def any_animal_to_dog(animal: Animal) -> Dog:
11    return Dog()
12
13process(any_animal_to_dog)  # OK

Generic 類別

建立自己的泛型容器

 1from typing import TypeVar, Generic, Optional
 2
 3T = TypeVar("T")
 4
 5class Stack(Generic[T]):
 6    """型別安全的堆疊"""
 7
 8    def __init__(self) -> None:
 9        self._items: list[T] = []
10
11    def push(self, item: T) -> None:
12        self._items.append(item)
13
14    def pop(self) -> Optional[T]:
15        if self._items:
16            return self._items.pop()
17        return None
18
19    def peek(self) -> Optional[T]:
20        if self._items:
21            return self._items[-1]
22        return None
23
24# 使用
25int_stack: Stack[int] = Stack()
26int_stack.push(1)
27int_stack.push(2)
28int_stack.push("three")  # 型別錯誤!
29
30str_stack: Stack[str] = Stack()
31str_stack.push("hello")

多型別參數

 1from typing import TypeVar, Generic
 2
 3K = TypeVar("K")
 4V = TypeVar("V")
 5
 6class Pair(Generic[K, V]):
 7    """鍵值對"""
 8
 9    def __init__(self, key: K, value: V) -> None:
10        self.key = key
11        self.value = value
12
13    def swap(self) -> "Pair[V, K]":
14        return Pair(self.value, self.key)
15
16# 使用
17pair: Pair[str, int] = Pair("age", 25)
18swapped: Pair[int, str] = pair.swap()

繼承泛型類別

 1from typing import TypeVar, Generic
 2
 3T = TypeVar("T")
 4
 5class Container(Generic[T]):
 6    def __init__(self, value: T) -> None:
 7        self.value = value
 8
 9# 方式一:保持泛型
10class Box(Container[T]):
11    def unwrap(self) -> T:
12        return self.value
13
14# 方式二:具體化型別
15class StringBox(Container[str]):
16    def upper(self) -> str:
17        return self.value.upper()

Protocol 與結構化子型別

什麼是 Protocol?

Protocol 定義「介面」,任何實現該介面的類別都被視為符合該 Protocol,無需明確繼承。

 1from typing import Protocol
 2
 3class Drawable(Protocol):
 4    def draw(self) -> str:
 5        ...
 6
 7# 這個類別沒有繼承 Drawable,但符合 Protocol
 8class Circle:
 9    def draw(self) -> str:
10        return "○"
11
12class Square:
13    def draw(self) -> str:
14        return "□"
15
16def render(shape: Drawable) -> None:
17    print(shape.draw())
18
19render(Circle())  # OK - Circle 有 draw() 方法
20render(Square())  # OK - Square 有 draw() 方法

Protocol vs ABC

特性ProtocolABC
繼承要求不需要需要
型別檢查結構化(duck typing)名義上(nominal)
執行期檢查需要 runtime_checkable內建
適用場景第三方類別、鬆散耦合自己控制的類別層級
 1from abc import ABC, abstractmethod
 2from typing import Protocol, runtime_checkable
 3
 4# ABC 方式
 5class DrawableABC(ABC):
 6    @abstractmethod
 7    def draw(self) -> str:
 8        ...
 9
10class CircleABC(DrawableABC):  # 必須繼承
11    def draw(self) -> str:
12        return "○"
13
14# Protocol 方式
15@runtime_checkable
16class DrawableProtocol(Protocol):
17    def draw(self) -> str:
18        ...
19
20class CircleProtocol:  # 不需要繼承
21    def draw(self) -> str:
22        return "○"
23
24# 執行期檢查
25print(isinstance(CircleProtocol(), DrawableProtocol))  # True

何時選擇 Protocol?

 1# 使用 Protocol 的情況:
 2
 3# 1. 處理第三方類別
 4class JSONSerializable(Protocol):
 5    def to_json(self) -> str:
 6        ...
 7
 8# 第三方類別可能已經有 to_json(),不需要修改它們
 9
10# 2. 定義回調介面
11class EventHandler(Protocol):
12    def on_event(self, event: dict) -> None:
13        ...
14
15# 任何有 on_event 方法的類別或函式都可以使用
16
17# 3. 鬆散耦合
18class Closeable(Protocol):
19    def close(self) -> None:
20        ...
21
22def cleanup(resource: Closeable) -> None:
23    resource.close()
24
25# 檔案、連接、任何有 close() 的東西都可以用

帶有屬性的 Protocol

 1from typing import Protocol
 2
 3class Named(Protocol):
 4    name: str  # 只需要有這個屬性
 5
 6class Person:
 7    def __init__(self, name: str) -> None:
 8        self.name = name
 9
10class Company:
11    name: str = "Acme Corp"
12
13def greet(entity: Named) -> str:
14    return f"Hello, {entity.name}!"
15
16greet(Person("Alice"))  # OK
17greet(Company())        # OK

實際範例:型別安全的 Repository 介面

結合以上所有概念,建立一個型別安全的資料存取層:

 1from typing import TypeVar, Generic, Protocol, Optional
 2from abc import abstractmethod
 3
 4# 定義實體必須有 id 屬性
 5class HasId(Protocol):
 6    id: int
 7
 8# 泛型 Repository 介面
 9T = TypeVar("T", bound=HasId)
10
11class Repository(Generic[T]):
12    """資料存取層的抽象介面"""
13
14    @abstractmethod
15    def get(self, id: int) -> Optional[T]:
16        """根據 ID 取得實體"""
17        ...
18
19    @abstractmethod
20    def save(self, entity: T) -> T:
21        """儲存實體"""
22        ...
23
24    @abstractmethod
25    def delete(self, id: int) -> bool:
26        """刪除實體"""
27        ...
28
29    @abstractmethod
30    def find_all(self) -> list[T]:
31        """取得所有實體"""
32        ...
33
34# 具體實現
35class User:
36    def __init__(self, id: int, name: str) -> None:
37        self.id = id
38        self.name = name
39
40class InMemoryUserRepository(Repository[User]):
41    """記憶體中的 User Repository"""
42
43    def __init__(self) -> None:
44        self._storage: dict[int, User] = {}
45        self._next_id = 1
46
47    def get(self, id: int) -> Optional[User]:
48        return self._storage.get(id)
49
50    def save(self, entity: User) -> User:
51        if entity.id == 0:
52            entity.id = self._next_id
53            self._next_id += 1
54        self._storage[entity.id] = entity
55        return entity
56
57    def delete(self, id: int) -> bool:
58        if id in self._storage:
59            del self._storage[id]
60            return True
61        return False
62
63    def find_all(self) -> list[User]:
64        return list(self._storage.values())
65
66# 使用
67def process_user(repo: Repository[User], user_id: int) -> None:
68    user = repo.get(user_id)
69    if user:
70        print(f"Found user: {user.name}")
71
72# 型別安全:不能把 UserRepository 傳給需要 Repository[Product] 的函式

常見錯誤

1. 忘記 TypeVar 的名稱參數

1# 錯誤
2T = TypeVar()  # TypeError
3
4# 正確
5T = TypeVar("T")

2. 在類別方法中重複定義 TypeVar

 1T = TypeVar("T")
 2
 3class Box(Generic[T]):
 4    # 錯誤:方法內重新定義 T
 5    def transform(self, func: Callable[[T], T]) -> T:
 6        T2 = TypeVar("T2")  # 這是不同的 TypeVar!
 7        ...
 8
 9    # 正確:直接使用類別的 T
10    def transform(self, func: Callable[[T], T]) -> T:
11        return func(self.value)

3. 誤用 covariant/contravariant

1T_co = TypeVar("T_co", covariant=True)
2
3class MutableBox(Generic[T_co]):  # 錯誤!
4    def __init__(self, value: T_co) -> None:
5        self._value = value
6
7    def set(self, value: T_co) -> None:  # 協變型別不能用在參數位置
8        self._value = value

小結

概念用途
bound限制 TypeVar 必須是某型別的子型別
covariant只讀容器,子型別可替代父型別
contravariant只寫容器,父型別可替代子型別
Generic[T]建立泛型類別
Protocol結構化子型別,不需繼承

思考題

  1. 為什麼 list 是不變的而不是協變的?
  2. 什麼情況下應該用 Protocol 而不是 ABC?
  3. 如何為一個既可讀又可寫的容器設計型別安全的介面?

下一章:3.5.2 異常設計架構