概要
前回は基本的なタイピングに入門しました
今回はリストや辞書などの少し複雑なタイピングを試してみました
記事内では List, Dict, TypedDict, Optional, Any, Literal, TypeVar を使ったサンプルコードとコメントをベースに紹介します
環境
- macOS 11.6.4
- Python 3.10.2
List や Dict を使ったタイピングのサンプルコード
"""リストや辞書のタイピングをテストするモジュール."""
from typing import List, Dict, Union
class Profile():
"""プロファイルを管理するクラス."""
def __init__(self):
"""お気に入り(リスト)とステータス(辞書)を初期化.
値は文字列と数値を許容するためUnionで定義する
"""
self.favorites: List[Union[str, int]] = []
self.status: Dict[str: Union[str, int]] = {}
def add_f(self, favorite: Union[str, int]) -> None:
"""お気に入りを1つ追加."""
self.favorites.append(favorite)
def add_s(self, key: str, value: Union[str, int]) -> None:
"""ステータスを1つ追加."""
self.status[key] = value
def show(self) -> None:
"""お気に入りとステータスを表示する."""
print(self.favorites)
print(self.status)
if __name__ == '__main__':
p = Profile()
p.add_f("game")
p.add_f(1)
p.add_s("name", "hawksnowlog")
p.add_s("age", 10)
p.show()
mypy を使ってエラーが出ないことを確認しましょう
List や Dict の値で文字列以外も扱いたい場合は Union を使うことで複数の型が入ってくることを定義することができます
TypedDict を使ったタイピングのサンプルコード
from typing import TypedDict
from dataclasses import dataclass
@dataclass
class Address():
"""住所情報を管理するクラス."""
zip: str
city: str
class Status(TypedDict):
"""ステータスを型ありの辞書として定義するクラス.
今回は辞書内に独自のクラス(Address)が値に入ることを想定して定義
"""
name: str
age: int
address: Address
class Profile():
"""プロファイルを管理するクラス."""
def __init__(self):
"""ステータス(型あり辞書)を初期化."""
self.status: Status = {}
def update(self, name: str, age: int, address: Address) -> None:
"""指定の値でステータスを更新."""
self.status['name'] = name
self.status['age'] = age
self.status['address'] = address
def show(self) -> None:
"""ステータスを表示する."""
print(self.status)
if __name__ == '__main__':
p = Profile()
p.update("hawksnowlog", 10, Address("123-4567", "Tokyo"))
p.update("hawksnowlog", "ten", Address("123-4567", "Tokyo"))
p.update("hawksnowlog", 10, Address("123-4567", None))
p.show()
ポイントは class Status(TypedDict)
のクラス定義で辞書内で定義する型情報をクラスとして定義します
先程の Dict と違ってクラスとして定義する必要があります
また None も今は許容していないので mypy でエラーになります
Optional を使ったタイピングのサンプルコード
"""Optionalのタイピングをテストするモジュール."""
from typing import TypedDict, Optional
from dataclasses import dataclass
@dataclass
class Address():
"""住所情報を管理するクラス.
city は None の指定も可能です
"""
zip: str
city: Optional[str]
class Status(TypedDict):
"""ステータスを型ありの辞書として定義するクラス.
今回は辞書内に独自のクラス(Address)が値に入ることを想定して定義
"""
name: str
age: int
address: Address
class Profile():
"""プロファイルを管理するクラス."""
def __init__(self):
"""ステータス(型あり辞書)を初期化."""
self.status: Status = {}
def update(self, name: str, age: int, address: Address) -> None:
"""指定の値でステータスを更新."""
self.status['name'] = name
self.status['age'] = age
self.status['address'] = address
def show(self) -> None:
"""ステータスを表示する."""
print(self.status)
if __name__ == '__main__':
p = Profile()
p.update("hawksnowlog", 10, Address("123-4567", "Tokyo"))
p.update("hawksnowlog", "ten", Address("123-4567", "Tokyo"))
p.update("hawksnowlog", 10, Address("123-4567", None))
p.show()
Optional は None を許容します
Address クラスの city フィールドで None を許容するように変更してみます
今度は mypy で None の部分がエラーにならないのが確認できると思います
Any を使ったタイピングのサンプルコード
"""Anyのタイピングをテストするモジュール."""
from typing import Any
from dataclasses import dataclass
@dataclass
class Address():
"""住所情報を管理するクラス.
city はどんな型の値でも指定可能です
"""
zip: str
city: Any
if __name__ == '__main__':
addr = Address("123-4567", "Tokyo")
addr = Address("123-4567", 123)
addr = Address("123-4567", addr)
addr = Address("123-4567", None)
入力される型が特定不可能なときに使います
例えば複数の外部サービスなどレスポンスが異なる場合に Any を使うことでいろいろなレスポンスが入ってくるということを明示することができます
Literal を使ったタイピングのサンプルコード
"""Literalのタイピングをテストするモジュール."""
from typing import Literal
from dataclasses import dataclass
@dataclass
class Address():
"""住所情報を管理するクラス.
city には Tokyo, Osaka, Fukuoka のみ指定可能です
"""
zip: str = "123-4567"
city: str = ""
CITY = Literal['Tokyo', 'Osaka', 'Fukuoka']
def set_city(self, city: CITY) -> None:
"""cityを設定します."""
self.city = city
def show(self) -> None:
"""cityを表示する."""
print(self.city)
if __name__ == '__main__':
addr = Address()
addr.set_city("Tokyo")
addr.show()
addr.set_city("Osaka")
addr.show()
addr.set_city("Fukuoka")
addr.show()
addr.set_city("Kyoto")
addr.show()
Literal は配列内に指定された値のみを許容することができるタイピングです
Union に近い感じはしますが Literal では型ではなく値のチェックをします
TypeVar を使ったタイピングのサンプルコード
"""TypeVarのタイピングをテストするモジュール."""
from typing import TypeVar
from dataclasses import dataclass
@dataclass
class Address():
"""住所情報を管理するクラス."""
zip: str = "123-4567"
city: str = ""
T = TypeVar('T', str)
def get_city(self) -> T:
"""cityを取得します.
city は T 型の変数が必ず返却されます
"""
return self.city
if __name__ == '__main__':
addr = Address()
print(addr.get_city())
これも Union に近いイメージです
Union は複合型ですが TypeVar は型変数になります
生成方法を見るとわかりますが TypeVar はメソッド呼び出しで生成されているので生成されるものが変数になります
Union は配列として定義しています
最後に
今回紹介したタイピング以外にも Set や FrozenSet といったジェネリック型も使うことができます
使用する変数やフィールドがどういった用途なのかに応じてこの辺りのタイピングができるようになるといいのかなと思います
ただ実行時にはどんな値の型でも入ってくるのでエディタやCI側でチェックするようにしましょう
参考サイト