概要
抽象クラスのメソッドが具象クラスのインスタンスを返す場合、その抽象基底クラスはジェネリック型にする必要がある。また、その型変数は抽象規定クラスを上界に指定する必要がある。
具体例
足し算と符号反転を定義すれば、引き算を自動で定義してくれる抽象クラスを考える。
from abc import ABC, abstractmethod
class AdditiveGroup(ABC):
@abstractmethod
def __add__(self, _):
pass
def __sub__(self, other):
return self.__add__(other.__neg__())
@abstractmethod
def __neg__(self):
pass
これを使って、整数を3で割った余りで分類する次のようなクラスを定義するとmod 3の世界で足し算、引き算、符号の反転が行える。
class FG3(AdditiveGroup):
def __init__(self, v):
self.v = v % 3
def __add__(self, other):
return FG3(self.v + other.v)
def __neg__(self):
return FG3(-self.v)
def __repr__(self):
return f"FG3({self.v})"
a = FG3(0)
b = FG3(1)
c = FG3(2)
print(a + b + c) # -> FG(0)
print(-c) # -> FG(1)
print(b - c) # -> FG(2)
ところで、当然ながらFG3のインスタンスにFG3のインスタンス以外を足したり引いたりしようとするとエラーになる。
print(a + 1) # -> AttributeError: 'int' object has no attribute 'v'
型ヒントを追加して、このバグを静的解析にて発見したい。
型ヒントの追加
具象クラスFG3の型ヒントは次の通り。__add__
と__neg__
でFG3
が必要だが、まだ定義されていないので文字列"FG3"
で代用する必要がある。
class FG3(AdditiveGroup):
v: int
def __init__(self, v: int) -> None:
self.v = v % 3
def __add__(self, other: "FG3") -> "FG3":
return FG3(self.v + other.v)
def __neg__(self) -> "FG3":
return FG3(-self.v)
def __repr__(self) -> str:
return f"FG3({self.v})"
あるいは、__future__.annotations
をインポートすればFG3
を使用できるようになる。
from __future__ import annotations
class FG3(AdditiveGroup):
v: int
def __init__(self, v: int) -> None:
self.v = v % 3
def __add__(self, other: FG3) -> FG3:
return FG3(self.v + other.v)
def __neg__(self) -> FG3:
return FG3(-self.v)
def __repr__(self) -> str:
return f"FG3({self.v})"
ややこしいのは抽象クラスの方で、 __add__
、__sub__
そして__neg__
の引数及び戻り値の型は具象クラスでなければならない。しかし、抽象クラスの定義時には将来どのクラスによって継承されるか分からない。そこで、ジェネリック型を利用する。
ところが、次のように単純にジェネリック型にしただけではエラーとなる。
T = TypeVar("T")
class AdditiveGroup(ABC, Generic[T]):
@abstractmethod
def add(self, _: T) -> T:
pass
def __sub__(self, other: T) -> T:
return self.__add__(other.__neg__())
@abstractmethod
def __neg__(self) -> T:
pass
原因は、__sub__
メソッドのself.__add__(other.__neg__())
で、other
が__neg__
を定義しているか、この時点では分からないためである。
実際のところ、T
型は抽象クラスAdditiveGroup
を継承しているはずで、other
は__neg__
を定義している。この条件がコードに表れていないのでエラーとなっている。
型変数T
に「AdditiveGroup
を継承していること」という条件を付けると、次のようになる。
T = TypeVar(“T”, bound="AdditiveGroup")
class AdditiveGroup(ABC, Generic[T]):
@abstractmethod
def add(self, _: T) -> T:
pass
def __sub__(self, other: T) -> T:
return self.__add__(other.__neg__())
@abstractmethod
def __neg__(self) -> T:
pass
ここでも、 型変数T
の定義時点ではAdditiveGroup
は未定義なので、bound="AdditiveGroup"
と文字列を使っている。この部分は、__future__.annotations
をインポートしていても文字列でなければならない。