【TypeHint】list[SubClass]がlist[SuperClass]に受け付けられない【Python】
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]
方の変数に代入することはできないんです!
これはtuple
やSequence
では起こりません。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
関数の中でChildB
をappend
するのは合法です。なぜってSuperClass
を継承したクラスのインスタンスだからですよね。
しかし呼び出し元のコードではchildrenはlist[ChildA]として定義されているのでその要素にはChildB
には存在しないChildA
としての機能を期待してしまいます。
このような事態を避けるためlist[SuperClass]
を受け取る変数にlist[ChildClass]
を渡してはいけないのです。それはlist
が可変オブジェクトだからです。tuple
なら許される理由もわかりますね?tuple
が変更不可能だからです。
まとめ
理屈はわかったけどSequence
とかIterable
インポートするの面倒なのでデフォルトで使えるようにしてほしい。
Discussion