2022年3月16日水曜日

Python でリストや辞書のタイピング入門

Python でリストや辞書のタイピング入門

概要

前回は基本的なタイピングに入門しました
今回はリストや辞書などの少し複雑なタイピングを試してみました
記事内では 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"))  # これは mypy でエラーになる
    p.update("hawksnowlog", 10, Address("123-4567", None))  # これは mypy でエラーになる
    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"))  # これは mypy でエラーになる
    p.update("hawksnowlog", 10, Address("123-4567", None))  # Optional で None を許容したので今度は mypy でエラーにならない
    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__':
    # 以下はすべてmypyでエラーにならない
    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")  # mypyでエラーになる
    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 1  # これは mypy エラー
        return self.city


if __name__ == '__main__':
    addr = Address()
    print(addr.get_city())

これも Union に近いイメージです
Union は複合型ですが TypeVar は型変数になります

生成方法を見るとわかりますが TypeVar はメソッド呼び出しで生成されているので生成されるものが変数になります
Union は配列として定義しています

最後に

今回紹介したタイピング以外にも Set や FrozenSet といったジェネリック型も使うことができます
使用する変数やフィールドがどういった用途なのかに応じてこの辺りのタイピングができるようになるといいのかなと思います

ただ実行時にはどんな値の型でも入ってくるのでエディタやCI側でチェックするようにしましょう

参考サイト

0 件のコメント:

コメントを投稿