見出し画像

Typing Generic Python3.12〜

Generiがわからん!ってことでGenericから逃げていましたが、

Python3.12〜なんか書き方が楽になったとのことなので、この機会に覚えようと思いました。

1. Genericってなに?

・「汎用的な型」を作る仕組み
※汎用的、抽象化これらの言葉は苦手だ。何をもって汎用なんだ・・・

ざっくりと・・・Genericを使うとAnyでは得られない
「この関数で使う型を呼び出し側が決められる」という効果が得られる。

この恩恵を受けたいためにGenericを使うって感じだと思います。

そもそも型ヒントは静的型付け言語のような「型チェックでエラーを早期に発見し大規模な開発をサポートする」というメリットを動的型付け言語であるPythonにも導入したいっていう理由で入れらています。

そのような状況でAnyを多用しちゃうと、せっかく型ヒントを使っているのに型ヒントの効力を最大限に発揮できず勿体無いという結果が発生します。

例えば

from typing import Any

def get_first_element(data: list) -> Any:
    return data[0]

def add(a: int, b: int) -> None:
    print(a + b)

arr = ["1", "2", "3"]
a = get_first_element(arr)

add(a, 3)

上記をmypyにかけると

mypy sample.py
Success: no issues found in 1 source file

って感じでパスしちゃうんですね。
※オプション--disallow-any-generics をつければエラーにできます。
VSCodeでもPylanceによる型チェックでのエラーが出ないので困ったものです。

当然このスクリプトを実行すると

TypeError: can only concatenate str (not "int") to str

strとintの足し算でエラーになります。これじゃ型チェックの意味が・・・って感じね

Any の性質が原因

  • Any は「どんな型とも互換性がある特殊な型」として扱われます。要するに万能すぎるんです。

  • つまり、Any 型は「何でもあり」であり、mypy の型チェックでは自動的にすべての型に適合してしまう。

ってことで、こんな万能野郎を使いすぎるとせっかくの型ヒントが勿体無い!って結果になりかねません。

Any を避けるために Generic を使う

これを避けるには、Any ではなくジェネリック型 (T) を使いましょう。ジェネリック型を使うことで、mypy が戻り値の型を厳密に推論できるようになります。

ある関数がリスト(リストじゃなくてもいいが)を返すときに、

リストであることは決まっている。
だけど、その中身のデータ型は決まっていない。
データ型は決まっていないけど、リストの中身は必ず全部同じ型ではある。

こんなときにGenericが使える。
Generic を使うと「中身の型」を伝えることができます!

def get_first_element[T](data: list[T]) -> T:
    return data[0]

def add(a: int, b: int) -> None:
    print(a + b)

arr = ["1", "2", "3"]
a = get_first_element(arr)

add(a, 3)

mypy使うと

sample.py:10: error: Argument 1 to "add" has incompatible type "str"; expected "int"  [arg-type]
Found 1 error in 1 file (checked 1 source file)

ってエラーを出してくれる。やった!

解説

  • [T] は型変数の宣言を意味します。

  • list[T] は、「リストの中身がすべて同じ型 T」であることを示します。

  • 返り値の型も T とすることで、「入力したリストの型が返り値の型と一致している」ことを保証します。

クラスでも使えるよ

class MyClass[T]:
    def __init__(self, data: T):
        self.data = data

1. データペアを扱うクラス(キーと値のペア)

class Pair[K, V]:
    def __init__(self, key: K, value: V):
        self.key = key
        self.value = value

    def get_key(self) -> K:
        return self.key

    def get_value(self) -> V:
        return self.value

pair = Pair[str, int]("age", 25)  # キーは str、値は int
print(pair.get_key())    # 出力: "age"
print(pair.get_value())  # 出力: 25

APIレスポンスのラッパー※これが一番使われているやつなのかな??

class APIResponse[T]:
    def __init__(self, status: int, data: T):
        self.status = status
        self.data = data

    def is_success(self) -> bool:
        return 200 <= self.status < 300

    def get_data(self) -> T:
        return self.data
response = APIResponse[str](200, "OK")  # レスポンスデータが文字列
print(response.is_success())  # 出力: True
print(response.get_data())    # 出力: "OK"

response2 = APIResponse[dict](404, {"error": "Not Found"})  # レスポンスデータが辞書
print(response2.is_success())  # 出力: False
print(response2.get_data())    # 出力: {"error": "Not Found"}

レスポンスデータの型を柔軟に変えられるので、API の処理を汎用化するのに便利そう。

覚えたことはどんどん試して身につける。

いいなと思ったら応援しよう!