👏

【TypeHint】list[SubClass]がlist[SuperClass]に受け付けられない【Python】

2022/11/14に公開

SubClassのlistはSuperClassのlistには代入できない

class SuperClass:
    pass


class ChildClass(SuperClass):
    pass


def process_children(children: list[SuperClass]):
    pass


def process_child(child: ChildClass):
    pass


child = ChildClass()
children = [child]
process_child(child)  # works
process_children(children)  # Argument of type "list[ChildClass]" cannot be assigned to parameter "children" of type  "list[SuperClass]"

Pythonでタイプセーフなコーディングを楽しんでいた今日気づきました。

SubClassのインスタンスをSuperClass型の変数に代入することはできるのにlist[SubClass]list[SuperClass]方の変数に代入することはできないんです!

これはtupleSequenceでは起こりません。listや一部のジェネリック型だけです。

list[SubClass]list[SuperClass]のような、ジェネリクス型引数の親子関係がそのまま受け継がれないような性質を不変(Invariant)といいます。

逆にtuple[Subclass]tuple[SuperClass]に代入できるので共変(Covariant)といいます。

先に結論:Sequence or Iterableを使う

from typing import Iterable


class SuperClass:
    pass


class ChildClass(SuperClass):
    pass


def process_children(children: Iterable[SuperClass]):
    pass


def process_child(child: ChildClass):
    pass


child = ChildClass()
children = [child]
process_child(child)  # works
process_children(children)  # works!

listのさらに上位概念であるSequenceやIterableをtypingモジュールからインポートして使いましょう。大抵の場合受け取った変数をfor文で回したりするだけなので、Iterableであることがわかっていれば十分です。

でもlist固有のappendメソッドが使いたいんですけど?

と思ったあなた。それはデザインからして間違いなのです。

それが許されるとすると次のようなコードが合法になります。

class SuperClass:
    pass


class ChildA(SuperClass):
    def cry(self):
        print("I am child a!")


class ChildB(SuperClass):
    pass


def process_children(children: list[SuperClass]):
    children.append(ChildB())  # SuperClassの子クラスなのでChildBを追加できる


child = ChildA()
children = [child]
process_children(children)
for cld in children:
    cld.cry()  # AttributeError 'ChildB' object has no attribute 'cry'

process_children関数の中でChildBappendするのは合法です。なぜってSuperClassを継承したクラスのインスタンスだからですよね。

しかし呼び出し元のコードではchildrenはlist[ChildA]として定義されているのでその要素にはChildBには存在しないChildAとしての機能を期待してしまいます。

このような事態を避けるためlist[SuperClass]を受け取る変数にlist[ChildClass]を渡してはいけないのです。それはlistが可変オブジェクトだからです。tupleなら許される理由もわかりますね?tupleが変更不可能だからです。

まとめ

理屈はわかったけどSequenceとかIterableインポートするの面倒なのでデフォルトで使えるようにしてほしい。

Discussion