Edited at

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

More than 1 year has passed since last update.


前提


引数の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のインスタンスをリストに追加されてしまうかもしれないからだ。