オブジェクト指向では、継承により既存の機能を引き継ぎつつ、自分のクラスに合わせて振る舞いを拡張します。
そのときに不可欠なのがsuper()
です。
親メソッドを正しく呼び出すことは、初期化漏れや重複実行を防ぎ、単一継承でも多重継承でも堅牢なクラス設計を実現します。
本記事では、Pythonにおけるsuper()
の基本から多重継承の協調的呼び出しまで、段階的に解説します。
super()とは? 親メソッド呼び出しの基本
Pythonのsuper()の役割とメリット
super()
は、現在のクラスのメソッド解決順序(MRO)に基づいて、次に呼ぶべき「親の実装」へのプロキシを返す関数です。
たとえばsuper().__init__()
と書くと、MRO上で次に位置するクラスの__init__
が呼ばれます。
- メリットは、継承構造の変更に強く、重複呼び出しや呼び漏れを避けやすいことです。
- Python 3では引数省略の
super()
が推奨で、見通しがよく、移植性も高いです。
superの本質は「親1つ」ではなく「MROに沿って次を呼ぶ」ことです。
これにより多重継承でも破綻しにくい協調的な呼び出しが実現します。
親クラス名で呼ぶ場合との違い
ParentClass.method(self,...)
と直接書くと、その親クラスだけが固定的に呼ばれます。
これは単一継承では一見問題なく動作しますが、多重継承では以下の問題が出ます。
- MROを無視して別系統を飛び越える
- 同じ祖先クラスのメソッドが重複実行される
- 継承順を変えたときに壊れやすい
多重継承では親クラス名直書きは高リスクです。
通例はsuper()
を用いた協調的呼び出しにします。
super()を使うべき場面
- 初期化(
__init__
)で親の設定を確実に通すとき - オーバーライドで親の前処理/後処理を拡張したいとき
- Mixinや多重継承で協調動作が必要なとき
- フレームワークのフックメソッド(
save
やdispatch
など)を拡張するとき
迷ったらsuper()
。
親名直書きは設計上の意図が明確な例外時のみと覚えると安全です。
単一継承でのsuper()の使い方
__init__で親の初期化を呼ぶ
単一継承でもsuper()
を使うことで、親の初期化を確実に実行します。
# 単一継承の基本例: 親の __init__ を呼ぶ
class Animal:
def __init__(self, name):
# 親の初期化
self.name = name
print(f"Animal.__init__ name={name}")
class Dog(Animal):
def __init__(self, name, breed):
# MROに従い、次の __init__ を呼ぶ(この場合は Animal)
super().__init__(name)
self.breed = breed
print(f"Dog.__init__ breed={breed}")
d = Dog("Pochi", "Shiba")
print(d.name, d.breed)
Animal.__init__ name=Pochi
Dog.__init__ breed=Shiba
Pochi Shiba
親の初期化をスキップすると属性未設定で後続処理が壊れます。
必ずsuper().__init__
を通しましょう。
オーバーライドから親メソッドを拡張
既存の動作に対して前処理や後処理を加える場合、親の実装をsuper()
で呼びつつ拡張します。
class TextProcessor:
def process(self, s: str) -> str:
print("TextProcessor.process - base")
return s.strip()
class LowerTextProcessor(TextProcessor):
def process(self, s: str) -> str:
print("LowerTextProcessor.process - before super")
# 親の結果を受け取り拡張
base = super().process(s)
print("LowerTextProcessor.process - after super")
return base.lower()
tp = LowerTextProcessor()
print(tp.process(" Hello World "))
LowerTextProcessor.process - before super
TextProcessor.process - base
LowerTextProcessor.process - after super
hello world
親呼び出しの位置(前/後)は要件次第です。
前処理なら先に、後処理なら後に置きます。
引数(*args, **kwargs)の受け渡し
将来の拡張に備えて、*args
/**kwargs
で柔軟に受け渡すと安全です。
特に多重継承では有効です。
class Base:
def __init__(self, *, debug=False, **kwargs):
# 余ったキーワードは次のクラスへ渡せる
super().__init__(**kwargs)
self.debug = debug
class Sub(Base):
def __init__(self, name, **kwargs):
# name は自分で処理、それ以外は親へ委譲
self.name = name
super().__init__(**kwargs)
s = Sub(name="item-1", debug=True)
print(s.name, s.debug)
item-1 True
キーワード引数を使い、不要なものはそのまま次へ流すのが拡張に強い設計です。
基本例(コード)とアンチパターン
良い例はsuper()
を使い、継承が変わっても壊れません。
# 良い例: super() ベース
class A:
def __init__(self):
super().__init__()
self.a = "A"
class B(A):
def __init__(self):
super().__init__()
self.b = "B"
print(B().__dict__)
{'a': 'A', 'b': 'B'}
アンチパターンは親名を直書きし、MROを壊します。
# 悪い例: 親クラス名を直書き
class A:
def __init__(self):
self.a = "A"
class B(A):
def __init__(self):
# A を直接呼ぶと、将来の多重継承時に重複・呼び漏れが起こる
A.__init__(self)
self.b = "B"
親名直書きはMROを無視し、ダイヤモンド継承で重複実行や初期化漏れの温床になります。
多重継承とMRO
MRO(メソッド解決順序)の基礎
PythonはC3線形化によりMROを決定します。
cls.__mro__
またはcls.mro()
で順序を確認できます。
class A: ...
class B(A): ...
class C(A): ...
class D(B, C): ...
print(D.mro())
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
MROではD→B→C→A→objectの順に解決されます。
superは常に「自分の次」を指すため、各クラスがsuper()
を呼ぶと、重複なく全員が一度ずつ呼ばれます。
ダイヤモンド継承での協調的super
以下は協調的super()
の典型です。
全員がsuper().__init__
を呼ぶことが重要です。
class A:
def __init__(self, **kwargs):
print("A.__init__")
super().__init__(**kwargs)
self.a = True
class B(A):
def __init__(self, **kwargs):
print("B.__init__")
super().__init__(**kwargs)
self.b = True
class C(A):
def __init__(self, **kwargs):
print("C.__init__")
super().__init__(**kwargs)
self.c = True
class D(B, C):
def __init__(self, **kwargs):
print("D.__init__")
super().__init__(**kwargs) # MROに基づき B → C → A → object
self.d = True
d = D()
print(d.a, d.b, d.c, d.d)
D.__init__
B.__init__
C.__init__
A.__init__
True True True True
誰か1人でもsuper()
を呼ばないと、チェーンが途切れて以降が実行されません。
Mixinクラスでの書き方のルール
Mixinは「横断的な小機能」を提供する継承用クラスです。
ルールは簡潔です。
- Mixinの
__init__
やオーバーライドしたメソッドでもsuper()
を必ず呼ぶ - コンストラクタのシグネチャは
def __init__(self, *args, **kwargs)
にして次へ渡す - 依存は明確にし、
super()
の呼び出しを前提に設計する
「自分は部分機能にすぎないので、最後までバトンを渡す」という意識が重要です。
super()を使わない時の問題点
- ダイヤモンドで祖先
A.__init__
が2回呼ばれる(副作用の二重実行) - 逆に祖先の初期化が1度も呼ばれない(属性未設定)
- 継承順を変えた瞬間に壊れる
多重継承でParent.__init__(self)
を直書きするのは避けるべきです。
__init__チェーンを切らない設計
- すべてのクラスが
super().__init__(...)
を呼ぶ - 受け取れない引数は
**kwargs
で受けて次へ渡す - 状態変更の副作用は冪等にし、重複呼び出しが起きても壊れにくくする
「どの順でも一度ずつ、最後まで届く」ことを常にテストで保証すると事故を防げます。
よくあるエラーとベストプラクティス
Python 3の引数省略super()を使う
Python 3では次のように簡潔に書けます。
class Foo:
def __init__(self):
super().__init__() # OK: 引数省略で現在のクラス/インスタンス文脈から解決
class Bar(Foo):
def __init__(self):
super().__init__()
古い書き方super(Bar, self)
は冗長です。
可読性と保守性のため省略形を使いましょう。
TypeError/AttributeErrorの原因と対処
- TypeError: unexpected keyword argument
- 原因: あるクラスが
**kwargs
を受けずに次へ渡せていない - 対処: 受け取らないクラスでも
**kwargs
を受け、super().__init__(**kwargs)
でバトンを渡す
- 原因: あるクラスが
- AttributeError: ‘X’ object has no attribute ‘y’
- 原因: 親の初期化を呼び忘れて属性が未設定
- 対処: 該当クラスに
super().__init__()
を追加し、テストで検出
- RuntimeError: super(): no arguments
- 原因: クラス外や関数スコープで引数省略
super()
を使った - 対処: クラス本文内のメソッドで使う。外で使う場合は
super(Class, instance)
形式にする
- 原因: クラス外や関数スコープで引数省略
例外メッセージを鵜呑みにせず、MROと引数伝播を点検すると根本原因に素早く辿り着けます。
親メソッド呼び忘れを防ぐパターン
テンプレートメソッド方式で、フックは小さく、親本体で共通処理をまとめます。
class Pipeline:
def run(self, data):
# 共通の骨格は親が保持
data = self.pre(data)
data = self.core(data) # サブクラスが実装
data = self.post(data)
return data
def pre(self, data): # デフォルトは何もしない
return data
def core(self, data):
raise NotImplementedError
def post(self, data):
return data
class MyPipeline(Pipeline):
def pre(self, data):
# 親の pre が将来なにかするかも…super を呼んで拡張
data = super().pre(data)
return f"[pre]{data}"
def core(self, data):
return data.upper()
p = MyPipeline()
print(p.run("ok"))
[pre]OK
サブクラスは極力拡張ポイントだけを小さくオーバーライドし、super()
で親の骨格処理を尊重します。
親クラス名直書きは最終手段
どうしても特定の祖先だけを狙って呼びたい、フレームワークの制約上MROをバイパスする必要がある、などの例外時のみ使います。
class A:
def f(self): print("A")
class B(A):
def f(self):
# 特定の祖先を明示して呼ぶ(最終手段)
A.f(self)
print("B")
この書き方はMROを壊す可能性があるため、設計上の意図と影響範囲を明記したうえで限定的に。
呼び出し順をテストで確認する
MROどおりに一度ずつ呼ばれているか、テストで機械的に確認します。
# 実行順をリストに記録して検証する簡易テスト
order = []
class A:
def __init__(self, **kwargs):
order.append("A")
super().__init__(**kwargs)
class B(A):
def __init__(self, **kwargs):
order.append("B")
super().__init__(**kwargs)
class C(A):
def __init__(self, **kwargs):
order.append("C")
super().__init__(**kwargs)
class D(B, C):
def __init__(self, **kwargs):
order.append("D")
super().__init__(**kwargs)
order.clear()
D()
print(order)
assert order == ["D", "B", "C", "A"], f"unexpected order: {order}"
['D', 'B', 'C', 'A']
テストで順序が固定されていると、継承順変更やMixin追加の影響を即座に検知できます。
まとめ
super()は、単なる「親を呼ぶ」道具ではなく、MROに沿って次を呼ぶ仕組みです。
単一継承でも多重継承でも、各クラスがsuper()
を呼び、*args
/**kwargs
で引数を次へ渡す協調的設計にすると、初期化漏れや重複実行のリスクを最小化できます。
エラーは引数伝播の欠落や呼び忘れが主因です。
引数省略superを使い、親名直書きは最終手段、テストで呼び出し順を確認という原則を守れば、継承構造が変わっても壊れにくい堅牢なクラスが書けます。