Python でジェネリック型の型パラメータを実行時に取れるようにする Mixin 作った
Python でジェネリック型の型パラメータをランタイム時に取得できる抽象を提供するライブラリ Reification を作りました。前回記事「Python のジェネリック型から実行時に型パラメータを取得する方法」の内容を綺麗に実装した感じです。
このライブラリを使用すると、ジェネリックなクラスにランタイムでその型パラメータを認識させ、それに基づいた動作をさせることができます。具体的なユースケースとしては、コードの動的型検査の強化や、型パラメータを動作上の意味付けとして利用することなどが考えられます。
以下のような感じで、プロパティを通して型パラメータへアクセスできるようになります。
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
クラスを作成し、push
と pop
メソッドを実装しています。
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
Python の型引数代入特殊メソッドは何でも入れれるので、
isinstance
で確認できないものが型引数に指定される可能性があります。 ↩︎