Python では super() は スーパークラスとは限らない
これは何?
タイトルに書いた
Python では super() は スーパークラスとは限らない
という事実を知ってびっくりしたので、どういうときにそうなるか、困りそうなポイント、なんかを考えたのでそのことを記した記事。
先にちょっと書いておくと。
super() が書かれているクラスをいくら見てもわからないクラスを指すことがあるよ、という話。
親の親のこともあるよ、とかではない。だったらびっくりしない。
super() って
親クラスのメソッドを呼ぶための関数だとかよく書いてあるけど、公式には違うことが書いてある(強調引用者)。
super([type[, object-or-type]])
メソッドの呼び出しを type の親または兄弟クラスに委譲するプロキシオブジェクトを返します。
ここを見るだけで、親じゃないことがあるということがわかる。そしてその先には
2 つ目の用途は、動的な実行環境下で協調的な多重継承をサポートすることです。
【略】
このメソッドをあらゆる場合に同じ形式で呼び出せるようにするのが、良い設計です (理由は、呼び出しの順序が実行時に決定されること、呼び出しの順序がクラス階層の変更に対応すること、呼び出しの順序に実行時まで未知の兄弟クラスが含まれる場合があることです)。
という不穏なことが書いてある。
事例
まずは普通の事例
たしかに普通は super() は スーパークラスの self みたいなものを返す。
class A:
def x(self):
return "A.x"
class B(A):
def x(self):
return "B.x > "+super().x()
print( B().x() )
super() に多くの人が期待しているのはこういうことだと思う。
super() が スーパークラスじゃない事例
super() が兄弟
import inspect
def try_call_x(su):
name = inspect.currentframe().f_back.f_code.co_qualname
try:
return f"{name} > "+su.x()
except Exception as e:
return f"{name} > EX:{e}"
class A1:
def x(self): return try_call_x(super())
class A2:
def x(self): return try_call_x(super())
class B(A1,A2):
def x(self): return try_call_x(super())
print( B().x() )
#=> B.x > A1.x > A2.x > EX:'super' object has no attribute 'x'
ご覧のとおり、 A1 内の super().x() は、 A2 の x() を呼ぶ。
ということで、super という名前の関数が(実質的に) スーパークラスじゃないクラスを指すことになっている。この現象は多重継承が使われている場合に起こり得る。
super() が親でも兄弟でもない
あと、公式には「親または兄弟クラス」とあったわけだけど、下記のように親でも兄弟でもないクラスになることもある。
import inspect
def try_call_x(su):
name = inspect.currentframe().f_back.f_code.co_qualname
try:
return f"{name} > "+su.x()
except Exception as e:
return f"{name} > EX:{e}"
class A1:
def x(self): return try_call_x(super())
class A2:
def x(self): return try_call_x(super())
class B1(A1,A2):
def x(self): return try_call_x(super())
class B2:
def x(self): return try_call_x(super())
class C(B1,B2):
def x(self): return try_call_x(super())
print( C().x() )
#=> C.x > B1.x > A1.x > A2.x > B2.x > EX:'super' object has no attribute 'x'
A2.x 内の super() は結果的に B2 だったわけだけど、継承関係は
親 ← → 子
A1 ┳ B1 ┳ C
A2 ┛ ┃
B2 ┛
なので、A2 から見た B2 は親でも兄弟でもない。家系図的には 義理の息子(義理の娘)かな。
ダイアモンド継承
ダイアモンド継承の場合は、下記の感じになる模様
import inspect
def try_call_x(su):
name = inspect.currentframe().f_back.f_code.co_qualname
try:
return f"{name} > "+su.x()
except Exception as e:
return f"{name} > EX:{e}"
class A1:
def x(self): return try_call_x(super())
class A2:
def x(self): return try_call_x(super())
class A3:
def x(self): return try_call_x(super())
class B1(A1, A2):
def x(self): return try_call_x(super())
class B2(A2, A3):
def x(self): return try_call_x(super())
class C(B1, B2):
def x(self): return try_call_x(super())
print( C().x() )
#=> C.x > B1.x > A1.x > B2.x > A2.x > A3.x > EX:'super' object has no attribute 'x'
# 親 ← → 子
# A1 - B1 ┳ C
# / ┃
# A2 ┃
# \ ┃
# A3 - B2 ┛
A1 の super() が A2 にならずに B2 になるところが面白い。
また、
親 ← → 子
B ┓
/ ┃
A ┃
\ ┃
X ┻ Y
のような継承ツリーがある場合、 B の super() は B にはスーパークラス A がいるにも関わらず、 A よりも X を優先する。
困るポイント
super().__init__()
親クラスがない (つまり、親クラスが object) のクラスの __init__ に super().__init__() を書くべきか否かで迷うかもしれない。
import inspect
def name():
return inspect.currentframe().f_back.f_code.co_qualname
class A1:
def __init__(self):
print(name())
class A2:
def __init__(self):
print(name())
class B(A1, A2):
def __init__(self):
super().__init__()
print(name())
B() #=> A2.__init__ が呼ばれない
上記の例で、A1 には親クラスがないんだから super().__init__() なんて書かなくていいだろうと思うんだけど、そうすると B の構築時に A2.__init__ が呼ばれない。困る。
じゃあ多重継承対策で super().__init__() を書いておけばいいかというとそうでもないし、実際 Python の標準ライブラリにある logging.Handler クラスの __init__ では super().__init__() などとは書かれていない。
なぜ「super().__init__() と書けばいい」とはならないのかは、下記の事例でわかると思う。
import inspect
def name():
return inspect.currentframe().f_back.f_code.co_qualname
class A:
def __init__(self):
print(name())
super().__init__()
class B1:
def __init__(self):
print(name())
super().__init__()
class B2:
def __init__(self, b2:str):
print(f"{name()}, b2={b2}")
self.b2_ = b2
class C1(A,B1):
def __init__(self):
print(name())
super().__init__()
c1 = C1()
# C1.__init__
# A.__init__
# B1.__init__
class C2(A,B2):
def __init__(self):
print(name())
super().__init__()
c2 = C2()
# C2.__init__
# A.__init__
# TypeError: B2.__init__() missing 1 required positional argument: 'b2'
上記の C1 は、A の __init__ 内の super().__init__() のお陰で B1 の初期化が滞りなく動いていてハッピーだが。
上記の C2 は、A の __init__ 内の super().__init__() と B2 の __init__ の引数が合わないのでエラーになる。
考えてみると、A が B2 のインスタンス化に必要な引数を知っているはずがない。
知っているのは C2 なので、C2 内で B2 の __init__ を呼ぶ必要がある。
ではどうすればいいのか
たぶん、
-
self以外に 引数のある__init__を持つクラスは、多重継承に参加するべきではない。
だと思う。
たとえば
import inspect
def name():
return inspect.currentframe().f_back.f_code.co_qualname
class A:
def __init__(self):
print(name())
class B(A):
def __init__(self):
print(name())
super().__init__()
b=B()
# B.__init__
# A.__init__
のような平和で当たり前っぽいクラス A, B があったとしても、下記のような X、Y を作ると B の __init__ でエラーになる。
class X(A):
def __init__(self, hoge:str):
print(name())
super().__init__()
self.hoge_ = hoge
class Y(B,X):
def __init__(self):
print(name())
super().__init__()
d = Y()
Y() を実行すると、B の __init__ 内の super().__init__() が X の __init__ を呼ぼうとしてしまうので、引数が合わない。シグニチャがあっていたとしても、X のインスタンス化に必要な正しい情報を B の __init__ が知っていると思えないのでどうしようもない。
それでもどうにかしたい
ここまでの事情を理解したうえでも多重継承したいということにはなかなかならないと思う。
私に関しても、ここまで書いてきた挙動が難儀すぎるので、Python で多重継承(実装やインスタンス変数を持つ複数のクラスをスーパークラスにするような多重継承)は使わないだろうと思っている。
とはいえ、それでも使いたいという方におすすめのプランを考えた。
※ 考えただけで、実践を伴わないので的外れかもしれない。
-
__init__にself以外の引数を持つ クラスは参加させない -
__init__で親クラスを初期化するコード- 親のないクラスなら
super().__init__()を書かない - 親が一人のクラスなら、
super().__init__()を書く - 親が複数のクラスなら
SuperA.__init__(self)SuperB.__init__(self)のようにクラスを指定して書く
- 親のないクラスなら
- ここまでのルールを守った場合、ダイアモンド継承があると同じ
__init__が複数回呼ばれる可能性があるが、それに耐えられないクラスなら参加させない
上記の指針を守れれば、多重継承を使っても困ることはないと思う。
あるいは ruby の様に
-
__init__を持つクラスに限れば一本道の継承ツリーになっている -
__init__が将来に渡って存在しないクラスであれば、上記のツリーに多重継承として参加可能
というルールでも良い。こちらのルールなら使う気になるかも。
それでも、メソッド名の衝突によって思わぬメソッドが呼ばれることが有り得ることには注意する必要がある。
他言語の場合
多重継承がある OOP 言語は C++ しか知らない(他にも存在は知っているけど、それらの言語のことを知らない)のでそのことを書くわけだけど。
C++ は
- Python の
super()のようなものや、MRO のようなルールはない - コンストラクタから兄弟クラスの初期化をすることは期待されてない
ということで、この記事にあるような問題は発生しない。
メソッド名の衝突も、C++ なら「曖昧だよ」とコンパイルエラーになりがちなのでそれほど心配はない。
ということで、C++ の場合、多重継承(実装やメンバ変数を持つ複数のクラスを基底クラスにするような多重継承)は恐れることなく使って良い機能だと思っている。
ただ、利用頻度は低い。低いのは、単に役に立つ場面があまりないから。あるいは、役に立つことに気づかないから。
最近は継承という言語機能を避けたい気持ちがあるのでなかなか使われにくいところだと思う。
まとめ
- Python で多重継承が絡んでいる場合、自分に親がいても
super()は兄弟や義理の子供なんかを指すことがあるよ。 - 自分に親がいても
super()が親以外を指すことがあるので、多重継承が絡んでいるときはsuper().__init__(a)が何を呼ぶのかは要注意だよ。-
__init__に限らないよ。
-
- 多重継承は、このあたりの事情を理解したうえで使わないと危険だよ。
-
__init__が無いクラスなら、まぁまぁ安全に多重継承に参加できるよ。
感想
super() がスーパークラスじゃないってのは、かなり思いがけないことだった。「名前重要」という指針は大事だと思う。
そこから掘っていったら、Python の多重継承はだいぶ危ういものだという印象になった。
そして逆に、 C++ の多重継承は(完璧ではないと思うけど)よくできてるなとか、Ruby の mix-in 戦略はいい考えなんだなと思った。
Discussion