名古屋出身ソフトウェアエンジニアのブログ

Python でジェネリック型の型パラメータを実行時に取れるようにする Mixin 作った

Python でジェネリック型の型パラメータをランタイム時に取得できる抽象を提供するライブラリ Reification を作りました。前回記事「Python のジェネリック型から実行時に型パラメータを取得する方法」の内容を綺麗に実装した感じです。

このライブラリを使用すると、ジェネリックなクラスにランタイムでその型パラメータを認識させ、それに基づいた動作をさせることができます。具体的なユースケースとしては、コードの動的型検査の強化や、型パラメータを動作上の意味付けとして利用することなどが考えられます。

curegit/reification: Reified generics in Python to get type parameters at runtime
Reified generics in Python to get type parameters at runtime - curegit/reification

以下のような感じで、プロパティを通して型パラメータへアクセスできるようになります。

from reification import Reified

class ReifiedList[T](Reified, list[T]):
    pass

xs = ReifiedList[int](range(10))
print(xs)  # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(xs.targ)  # <class 'int'>

インストール

PyPI からインストールできます。 Python >= 3.12 が必要です。

pip3 install reification

ライブラリの使い方

公開 API はすべて reification パッケージのトップレベルに定義されています。唯一の API である Reified クラスとその機能は以下の通りです。

Reified(クラス)

このクラスは、型パラメータに基づいて新しい型を作成するための Mixin です。

このクラスはスレッドセーフであり、継承したクラスは複数のスレッドで使用することができます。

また、このクラスは直接インスタンス化することはできません(必ず継承させます)。

targ: type | tuple[type | Any, ...] | Any(クラスプロパティ)

ジェネリッククラスに指定された型引数を表します。型引数が複数ある場合、targ は型引数のタプルになります。

Python の型引数には何でも渡せるため、型注釈に Any を含めています。

type_args: tuple[type | Any, ...](クラスプロパティ)

targ と同じですが、type_args は型引数が一つであっても常にタプルを返します。

使用例: 実行時型検査付きのジェネリックスタック

実際の使用例を示します。ここでは、Reified を継承してジェネリックな ReifiedStack クラスを作成し、pushpop メソッドを実装しています。

from reification import Reified


class ReifiedStack[T](Reified):
    def __init__(self) -> None:
        super().__init__()
        self.items: list[T] = []

    def push(self, item: T) -> None:
        # We can do runtime check
        if isinstance(item, self.targ):
            self.items.append(item)
        else:
            raise TypeError()

    def pop(self) -> T:
        if self.items:
            return self.items.pop()
        else:
            raise IndexError("pop from empty stack")


stack = ReifiedStack[str]()
stack.push("spam")  # OK
stack.push(42)  # raises TypeError

push メソッドでは、アイテムが指定された型(Reified から継承された targ プロパティでアクセス可能)であるかを実行時に確認します1。型が一致しない場合、TypeError が発生します。

この例では、str を型引数として ReifiedStack のインスタンスを作成しています。"spam" という文字列のプッシュはスタックの型引数と一致するため受け入れられます。しかし、42 という整数をプッシュすると、スタックの型引数と整合しないため、TypeError が発生します。

同値性

Reified 派生クラスに型引数を与えて特殊化した型は、型自体と型引数が同じであれば同じ型とみなされ、Python の型意味論的に尊重されます。よって、組み込み関数 isinstance は、Reified 派生型とシームレスに動作します。

>>> isinstance(ReifiedList[int](), ReifiedList[int])
True
>>> isinstance(ReifiedList[str](), ReifiedList[int])
False

部分型付け

ノミナルサブタイピングをサポートしています。型引数の共変性・反変性は考慮されず、常に非変のように振る舞います。

>>> issubclass(ReifiedList[int], ReifiedList[int])
True
>>> issubclass(ReifiedList, ReifiedList[int])
False
>>> issubclass(ReifiedList[int], ReifiedList)
True
>>> issubclass(ReifiedList[str], ReifiedList[int])
False
>>> class ReifiedListSub(ReifiedList[int]):
...     pass
...
>>> issubclass(ReifiedListSub, ReifiedList[int])
True

  1. Python の型引数代入特殊メソッドは何でも入れれるので、isinstance で確認できないものが型引数に指定される可能性があります。 ↩︎