Python TypedDict with Generics

Alex O'Callaghan

Python 結合泛型的 TypedDict

原文由 Alex O'Callaghan 發布,訂閱此部落格

Python 在 3.5 版加入了對型別提示的支援,但要用泛型型別來標註 dict 的值,卻意外地有點棘手。

如果要為擁有一組固定鍵的字典定義型別提示,使用 typing 模組中的 TypedDict 類別會相當直觀:

from typing import TypedDict

class Book(TypedDict):
    id: int
    name: str

book: Book = {"id": 123, "name": "To Kill A Mockingbird"}

如果再加上這一行:

not_a_book: Book = {"something": "A book doesn't have"}

再用 mypy 執行,就會得到:

$ python -m mypy type-dict.py
type-dict.py:9: error: Missing keys ("id", "name") for TypedDict "Book"  [typeddict-item]
type-dict.py:9: error: Extra key "something" for TypedDict "Book"  [typeddict-unknown-key]
Found 2 errors in 1 file (checked 1 source file)

就這麼簡單!

typing 模組也提供了 GenericTypeVar 來支援泛型類別。以我的情況來說,我想為一個帶有 value 鍵的巢狀 dict 定義一個泛型類別,而這個鍵的值型別是可以變動的。沿用上面的書籍範例:

from typing import TypeVar, Generic, TypedDict

T = TypeVar('T')

class Attribute(TypedDict, Generic[T]):
  value: T

class Book(TypedDict):
  id: Attribute[int]
  name: Attribute[str]

book: Book = {"id": {"value": 123}, "name": {"value": "To Kill A Mockingbird"}}

mypy 做型別檢查,一切正常!不過,試著實際執行看看……

$ python --version
Python 3.10.12
$ python type-dict-generic.py
Traceback (most recent call last):
  File "/home/aocallaghan/exp/py-type-dict/type-dict-generic.py", line 5, in <module>
    class Attribute(TypedDict, Generic[T]):
  File "/home/aocallaghan/.pyenv/versions/3.10.12/lib/python3.10/typing.py", line 2348, in __new__
    raise TypeError('cannot inherit from both a TypedDict type '
TypeError: cannot inherit from both a TypedDict type and a non-TypedDict base class

在 Python 3.10 中,你無法同時繼承 TypedDict 型別和其他類別。不過這個問題在 Python 3.11 中已經被修正

$ python --version
Python 3.11.4
$ python type-dict-generic.py

很可惜,在 3.11 之前的版本中,似乎沒有簡單的方法可以達成這個需求。其中一個選項是使用 Enum 類別來表示鍵:

from enum import Enum, auto
from typing import TypeVar, TypedDict

T = TypeVar('T')

class AttributeKeys(Enum):
  value = auto()

class Book(TypedDict):
  id: dict[AttributeKeys, int]
  name: dict[AttributeKeys, str]

book: Book = {"id": {AttributeKeys.value: 123}, "name": {AttributeKeys.value: "To Kill A Mockingbird"}}

not_a_book: Book = {"something": "A book doesn't have"}

本文章由 muse-spark-1.2-contributor 進行翻譯

留言