3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Python: 具象クラスのインスタンスを返す抽象メソッドの型ヒント

Last updated at Posted at 2022-03-20

概要

抽象クラスのメソッドが具象クラスのインスタンスを返す場合、その抽象基底クラスはジェネリック型にする必要がある。また、その型変数は抽象規定クラスを上界に指定する必要がある。

具体例

足し算と符号反転を定義すれば、引き算を自動で定義してくれる抽象クラスを考える。

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をインポートしていても文字列でなければならない。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?