クラスを継承して書き始めたとき、super().__init__()という一行を見たことはありませんか。写経したコードに書いてあったから、とりあえず自分も同じように書いている。そんな方は多いはずです。
私も最初はそうでした。おまじないだと思って並べていて、何をしている行なのかは説明できませんでした。
けれどこの一行には、はっきりとした役割があります。しかも、知らずに省くと静かにバグを作ってしまう種類の役割です。
動かしながら、順番に見ていきましょう。
super()を書き忘れると、親の初期化がまるごと消える¶
仕組みの話より先に、失敗を見たほうが早いです。動物を表すクラスと、それを継承した犬のクラスを書いてみます。
手元のPython 3.11で動かした結果を、そのまま載せます。
class Animal:
def __init__(self, name):
self.name = name
class Dog(Animal):
def __init__(self, name, trick):
self.trick = trick
dog = Dog('ポチ', 'おすわり')
print(dog.trick) # おすわり
print(dog.name)
# AttributeError: 'Dog' object has no attribute 'name'
trickは取れるのに、nameだけが存在しないと言われました。継承しているのだからnameもあるはずだ、と思いたくなるところです。
原因は、Dogが自分の__init__を用意したことにあります。子クラスが__init__を定義すると、親の__init__は自動では呼ばれません。
つまりself.name = nameという一行が、一度も実行されていないのです。継承されるのはメソッドであって、親の初期化処理が勝手に走るわけではありません。
ここで登場するのがsuper()です。一行足すだけで直ります。
class Dog(Animal):
def __init__(self, name, trick):
super().__init__(name)
self.trick = trick
dog = Dog('ポチ', 'おすわり')
print(dog.name, dog.trick) # ポチ おすわり
厄介なのは、書き忘れてもその場ではエラーが出ないところです。dog.nameを実際に使う行まで進んで、はじめて落ちます。
そもそも__init__がなぜこういう名前なのか気になった方は、先に特殊メソッドの全体像をつかんでおくと理解が早まります。【関連記事】Pythonの特殊メソッドとは?どんな種類があるのか?どうやって使うのか?
super()は親クラスを指しているわけではない¶
ここまでだと、super()は親クラスの別名のように見えます。実際、そう説明している入門書も少なくありません。
けれど、これは正確ではありません。公式ドキュメントはsuper()を、親またはきょうだいクラスへメソッド呼び出しを委譲するプロキシオブジェクトだと説明しています。
きょうだいクラス、という言葉が引っかかりませんか。親ではないクラスへ話が飛ぶことがある、と書いてあるわけです。
その正体を確かめるために、クラスが内側に持っている順番のリストをのぞいてみます。
MROという順番リストを見てみる¶
Pythonのクラスは、メソッドを探す順番をあらかじめ一列に並べて持っています。これをMRO(Method Resolution Order、メソッド解決順序)と呼びます。
__mro__という属性から、そのまま見られます。
print([c.__name__ for c in Dog.__mro__])
# ['Dog', 'Animal', 'object']
Dog、Animal、objectの順に並んでいました。メソッドを呼ぶと、Pythonはこの列を上から順に探していきます。
そしてsuper()がすることは、この列の中で自分の次にいるクラスへ処理を渡すこと、ただそれだけです。親クラスを名指ししているわけではありません。
単純な継承なら、次にいるのはたしかに親です。だから親の別名だと思い込んでいても、しばらくは困りません。
同じsuper()が、呼ばれ方で行き先を変える¶
食い違いが表に出るのは、複数のクラスを継承したときです。ここがsuper()の本領であり、同時にいちばん誤解されている部分でもあります。
似た形のクラスを4つ用意して、同じメソッドを呼び比べてみます。
class L:
def go(self): print('L')
class M(L):
def go(self):
print('M'); super().go()
class N(L):
def go(self):
print('N'); super().go()
class O(M, N): pass
M().go()
# M
# L
O().go()
# M
# N
# L
注目してほしいのは、M.goのコードが一文字も変わっていないことです。それなのに、super().go()の行き先だけが変わりました。
Mのインスタンスから呼べば、次はLです。Oのインスタンスから呼べば、次はNになります。
種明かしは、それぞれのMROにあります。並べて印刷してみましょう。
print([c.__name__ for c in M.__mro__]) # ['M', 'L', 'object']
print([c.__name__ for c in O.__mro__]) # ['O', 'M', 'N', 'L', 'object']
Oの列では、Mの次にNが割り込んでいました。super()が見ているのは、書かれた場所の親ではなく、実際に呼ばれたインスタンスの順番リストです。
親の名前を直書きすると、途中のクラスが飛ばされる¶
ではsuper()を使わず、親クラスの名前を直接書いたらどうなるでしょうか。一見すると、こちらのほうが素直で分かりやすく見えます。
同じ形の継承を、名前の直書きで組んでみます。
class Base:
def setup(self): print('Base.setup')
class A(Base):
def setup(self):
print('A.setup'); Base.setup(self)
class B(Base):
def setup(self):
print('B.setup'); Base.setup(self)
class C(A, B): pass
C().setup()
# A.setup
# Base.setup
B.setupが、どこにも出てきませんでした。CはBも継承しているはずなのに、その処理だけが丸ごと抜け落ちています。
AがBaseを名指しした瞬間に、列の途中にいるBを飛び越えてしまったからです。同じクラスをsuper()に書き換えると、結果は変わります。
class A(Base):
def setup(self):
print('A.setup'); super().setup()
class B(Base):
def setup(self):
print('B.setup'); super().setup()
class C(A, B): pass
C().setup()
# A.setup
# B.setup
# Base.setup
今度は3つとも呼ばれました。全員がsuper()を使うことで、MROの列が端から端まできれいに流れています。
この性質を、公式ドキュメントは協調的多重継承と呼んでいます。全員がsuper()を使ってはじめて成立する仕組みで、一人でも名前を直書きするとそこで列が途切れます。
ちなみに、順番を一列に決められない継承を書いた場合は、Pythonがクラスを作る時点で止めてくれます。
class X: pass
class Y: pass
class P(X, Y): pass
class Q(Y, X): pass
class R(P, Q): pass
# TypeError: Cannot create a consistent method resolution
# order (MRO) for bases X, Y
継承そのものの設計で迷いやすい方は、先に3大要素を押さえておくと読み解きやすくなります。【関連記事】継承・カプセル化・ポリモーフィズム。オブジェクト指向の3大要素をPythonで理解
2引数のsuper()は書き換え忘れで壊れる¶
少し古い記事を読んでいると、super(Dog, self)のように引数を2つ渡す書き方を見かけます。これはPython 2の時代の書き方で、今でも動きはします。
ただ、これから書くコードでは引数なしのsuper()を使ってください。理由はひとつで、クラス名を手で書いている箇所がそのまま事故の種になるからです。
最初のDogをこの書き方に直したうえで、クラスごとコピーして別の名前に変えてみます。
class Dog(Animal):
def __init__(self, name, trick):
super(Dog, self).__init__(name) # 昔の書き方。ここまでは動く
self.trick = trick
class Puppy(Animal):
def __init__(self, name, trick):
super(Dog, self).__init__(name) # Dog のまま直し忘れた
self.trick = trick
print(Dog('ポチ', 'おすわり').name) # ポチ
Puppy('コロ', 'お手')
# TypeError: super(type, obj): obj must be an instance or subtype of type
エラーで止まってくれるだけまだ親切ですが、直すまで動きません。引数なしで書いていれば、そもそも直す場所が存在しません。
3つの書き方を、判断しやすいように並べておきます。
| 書き方 | 動くか | 使うべきか |
|---|---|---|
super().__init__() |
動く | これを使う。クラス名を書かないので壊れない |
super(Dog, self).__init__() |
動く | Python 2時代の書き方。名前の直し忘れで壊れる |
Animal.__init__(self) |
動く | 多重継承で途中のクラスを飛ばす。避ける |
引数なしのsuper()が成立しているのは、Pythonがメソッドへ__class__という情報をこっそり持たせているからです。この仕組みはPEP 3135として提案され、Python 3から使えるようになりました。
super()が使えない場所もある¶
裏で仕込まれている都合上、引数なしのsuper()はクラスの中でしか使えません。同じコードを普通の関数に切り出すと、その瞬間に動かなくなります。
実際に試すと、こうなります。
def f():
return super()
f()
# RuntimeError: super(): no arguments
メソッドをクラスの外へ移すリファクタリングのときに、ここで一度つまずきます。引数なしのsuper()はクラスの中にいることが前提だと覚えておいてください。
クラス設計には、これと同じように見た目からは気づけない落とし穴がほかにもあります。【関連記事】Pythonの名前マングリングとは?知らないとハマるクラス設計の落とし穴を徹底解説
実務でのsuper()との付き合い方¶
私は10年ほどエンジニアとして開発に関わってきましたが、super()がらみで一度だけ本気で困ったことがあります。
Webアプリのユーザー登録まわりで、共通の保存処理を持つ基底クラスに、ログを追加するクラスと通知を送るクラスを組み合わせて使っていました。
ある時期から、通知だけが飛ばなくなりました。例外は出ず、ログにも何ひとつ残りません。
原因は、ログを追加するクラスの中で基底クラスを名前で直接呼んでいたことでした。あいだに挟まっていた通知クラスが、静かに飛ばされていたのです。
書いた本人に落ち度があったかというと、微妙なところです。当時は単一継承だったので正しく動いていて、あとから別の人が多重継承へ組み替えた時点で壊れました。
この件から、継承を書くときは2つだけ自分に課しています。オーバーライドしたメソッドでは必ずsuper()を呼ぶこと、そして継承の形に迷ったら__mro__を印刷して目で確かめることです。
もっと言えば、多重継承を避けられるならそれがいちばんです。共通処理を親クラスへ押し込むより、部品として持たせたほうが壊れにくい場面は多くあります。
クラスの役割をはっきりさせたいときは、抽象クラスという選択肢も合わせて検討してみてください。【関連記事】Pythonの抽象クラス(ABC)とは?実装のし忘れを防ぐ設計の基本を初心者向けに解説
まとめ¶
子クラスに__init__を書くと、親の__init__は自動では呼ばれません。super().__init__()は、その抜けを埋めるための一行です。
そしてsuper()が指しているのは、親クラスそのものではありません。実際に呼ばれたインスタンスのMROの中で、自分の次にいるクラスです。
だから親の名前を直書きすると、多重継承のときに途中のクラスを飛ばしてしまいます。しかもエラーは出ず、ただ処理が実行されないだけという厄介な形で表れます。
引数なしのsuper()を使い、迷ったら__mro__を印刷して順番を確かめる。この2つを守っておけば、継承まわりで静かに壊れることはぐっと減ります。
まずはお手元のコードで、オーバーライドしたメソッドの中に親クラスの名前を直接書いている場所がないか探してみてはいかがでしょうか。ここまでお読みいただきありがとうございました。