Python
継承
typing

List[Foo]を返すメソッドは継承しにくい

前提

引数のList[T]は論外

Pythonのtypingを使ってお型付けをしているときの話。

from typing import List


class Bar:
    def list_to_none(self, foo: List[Foo]) -> None:  # Bad
        for f in foo:
            self.something(f)

こういうメソッドを書くのはよろしくない。
中でやっていることはiterationでしかないのに、listを継承しているものしか受け取れないようになっているからだ。
この場合は、できるだけlistに近い構造を期待している場合はSequence[T]を、単にiterationさえできればいいならCollection[T]を受け取るようにすればよい。

from typing import Sequence


class Bar:
    def seq_to_none(foo: Sequence[Foo]) -> None:  # Good!
        for f in foo:
            self.something(f)

本題

返り値のList[T]も状況によってはよろしくない

from typing import List


class Bar:
    def __init__(self, N: int) -> None:
        self._foo = [Foo() for i in range(N)]

    def none_to_list(self) -> List[Foo]:  # Good...?
        return self._foo

返り値は自分で決めるものなので、受け取る側の引数と違ってできるだけ表す内容が狭い方がいい。
なのでCollectionSequenceといった広い型ではなく、Listを使って表したくなる。

だが、このクラスを継承する場合を考えると、話は変わってくる。

from typing import List


class Bar:
    def __init__(self, N: int) -> None:
        self._foo = [Foo() for i in range(N)]

    def none_to_list(self) -> List[Foo]:
        return self._foo


class MyFoo(Foo):
    pass


class BarOfMyFoo(Bar):
    def __init__(self, N: int) -> None:
        self._myfoo = [MyFoo() for i in range(N)]

    def none_to_list(self) -> List[MyFoo]:
        # Mypy Error
        # Return type of "none_to_list" incompatible with supertype "Foo"
        return self._myfoo

この継承はエラーになる。というのも、Python の Generics の引数には covariant (共変) と contravariant (反変) と invariant (不変) があり、Collection[T]Sequence[T]が共変なのに対してList[T]は不変だからである。なので、これをSequenceに変えるとエラーは消える。

from typing import Sequence


class Bar:
    def __init__(self, N: int) -> None:
        self._foo = [Foo() for i in range(N)]

    def none_to_seq(self) -> Sequence[Foo]:
        return self._foo


class MyFoo(Foo):
    pass


class BarOfMyFoo(Bar):
    def __init__(self, N: int) -> None:
        self._myfoo = [MyFoo() for i in range(N)]

    def none_to_seq(self) -> Sequence[MyFoo]:  # No error
        return self._myfoo

ちなみに、Listのまま返り値の中身をFooであると宣言してもエラーは消えてくれない。

from typing import List


class Bar:
    def __init__(self, N: int) -> None:
        self._foo = [Foo() for i in range(N)]

    def none_to_list(self) -> List[Foo]:
        return self._foo


class MyFoo(Foo):
    pass


class BarOfMyFoo(Bar):
    def __init__(self, N: int) -> None:
        self._myfoo = [MyFoo() for i in range(N)]

    def none_to_list(self) -> List[Foo]:
        return self._myfoo
        # Invokes another mypy Error
        # Incompatible return value type (got "List[MyFoo]", expected "List[Foo]")

MyFooFooの子だと認識されるが、List[MyFoo]List[Foo]の子にはなりえないという話である。

継承されることを想定したクラスを書く際は注意してほしい。

追記:
もう少しだけ詳しく書いておくと、ListCollections.abc.MutableSequenceのサブクラスだが、SequenceCollections.abc.ImmutableSequenceのサブクラスなので、返したリストを変更される予定がないならSequenceを返すのがよい。
返したリストを変更される予定が少しでもあるなら、継承でList[MyFoo]を返してはいけない。なぜなら、変更する側は中身をFooだと思って変更を行うので、例えばFooのインスタンスをリストに追加されてしまうかもしれないからだ。