まえがき
前回に引き続き、今回は継承について扱う。
継承
継承:元のクラスのメンバーを引き継ぎながら、新たな機能を加えたり、元の機能上書きする仕組み
→ 機能の共通した部分を切り出して、差分だけを書いていく仕組み(差分プログラミング)
基底クラス(親クラス、スーパークラス):継承元となるクラスのこと
派生クラス(子クラス、サブクラス):継承してできたクラス
継承の基本
継承の一般的な構文は以下。classブロックでクラス名の後方に継承元(基底クラス)を指定。
class 派生クラス名(基底クラス名, ...):
...派生クラスの定義...
基底クラスを省略した場合は、暗黙的にオブジェクトクラスを継承したとみなされる(これまでのクラス定義はこのパターン)
→ Pythonのクラスは直接/間接を問わず、最終的にobjectクラスを継承する。
以下はPersonクラスを継承し、BusinessPersonクラスを定義する例。
class Person:
def __init__(self, firstname: str, lastname: str) -> None:
self.firstname = firstname
self.lastname = lastname
def show(self) -> None:
print(f'私の名前は{self.lastname}{self.firstname}です!')
# Personを継承したBusinessPersonクラスを作成
class BusinessPerson(Person):
def work(self) -> None:
print(f'{self.lastname}{self.firstname}は働いています。')
if __name__ == '__main__':
bp = BusinessPerson('太郎', '山田')
bp.show() # 私の名前は山田太郎です
bp.work() # 山田太郎は働いています
上記のコードの注意点は以下
・基底クラスよりも具体的な名前を派生クラスに与える
→ 末尾に基底クラスの名前を付与すれば、互いの継承関係をより把握しやすくなるようにするべき(逆に基底クラスは派生クラスの一般的な特徴を表した名前であるべき)
・複数の基底クラスを作ってもよい
class BusinessPerson(Person, Hoge):
のように、1つのクラスが複数のクラスを親に持つような継承(多重継承)を認めている。
・派生クラスにメンバーを追加する
上では、派生クラス独自のメソッドとしてworkメソッドを定義
→ workメソッドを呼び出せる & 基底クラスで定義されたshowメソッドが、あたかもBusinessPersonクラスのメンバーであるかのように呼び出せる
継承は、要求されたメンバーを現在のクラスから検索し、存在しない場合は上位のクラスからメンバーを検索し呼び出す。
どのような場合に継承を利用するといいのか?
基底クラスと派生クラスに、is-aの関係が成り立つ場合に用いるのがよい(派生クラス⊂基底クラス)。
メソッドのオーバーライド
オーバーライド:継承を利用することで、基底クラスで定義されたメソッドを派生クラスで上書きすることが出来る
以下は、BusinessPersonクラスを継承して、EliteBusinessPersonクラスを定義する例。
class Person:
def __init__(self, firstname: str, lastname: str) -> None:
self.firstname = firstname
self.lastname = lastname
def show(self) -> None:
print(f'私の名前は{self.lastname}{self.firstname}です!')
class BusinessPerson(Person):
def work(self) -> None:
print(f'{self.lastname}{self.firstname}は働いています。')
class EliteBusinessPerson(BusinessPerson):
def work(self) -> None:
print(f'{self.lastname}{self.firstname}はバリバリ働いています。')
if __name__ == '__main__':
bp = EliteBusinessPerson('太郎', '山田')
bp.work() # 山田太郎はバリバリ働いています。
bp.show() # 私の名前は山田太郎です!
workメソッドはEliteBusinessPersonクラスで定義されているもので、結果も(BusinessPersonクラスのworkメソッドではなく)EliteBusinessPersonクラスのworkメソッドを実行した結果が得られる。
showメソッドはオーバーライドされていないので、EliteBusinessPersonクラスの大元に基底クラスであるPersonクラスで定義されたshowメソッドが実行されている。
→ クラスは多段階にわたって継承できる。
superクラスによる基底クラスの参照
super関数を用いることで、派生クラスから基底クラスのメソッドを呼び出す
→ 基底クラスの処理を引き継ぎつつ、派生クラスで差分の処理だけ追加することも可能になる。
super().メソッド名(引数, ...)
以下は、BusinessPersonクラスを継承して、新たにHetareBusinessPersonクラスを定義するコード。
class Person:
def __init__(self, firstname: str, lastname: str) -> None:
self.firstname = firstname
self.lastname = lastname
def show(self) -> None:
print(f'私の名前は{self.lastname}{self.firstname}です!')
class BusinessPerson(Person):
def work(self) -> None:
print(f'{self.lastname}{self.firstname}は働いています。')
class HetareBusinessPerson(BusinessPerson):
def work(self) -> None:
super().work()
print('ただし、ボチボチと...')
if __name__ == '__main__':
hbp = HetareBusinessPerson('太郎', '山田')
hbp.work() # 山田太郎は働いています。ただし、ボチボチと...
基底クラスであるBusinessPersonのworkメソッドを呼び出したうえで、super()関数を用いて、HetareBusinessPerson独自の処理を記述している。また、hbp.work()においても、派生クラスの結果に基底クラスの結果が加わっていることが確認できる。
初期化メソッドのオーバーライド
基底クラスの初期化メソッドをオーバーライドする場合を考える。
以下は、Personクラスのインスタンス変数firstname/lastnameに加えて、middleクラスを追加したForeignerクラスを定義する例。
class Person:
def __init__(self, firstname: str, lastname: str) -> None:
self.firstname = firstname
self.lastname - lastname
def show(self) -> None:
print(f'私の名前は{self.lastname}{self.firstname}です!')
class Foreigner(Person):
def __init__(self, firstname: str, lastname: str) -> None:
# 基底クラスのコンストラクターを呼び出し
super().__init__(firstname, lastname)
# 独自のmiddlenameを初期化
self.midllename = middlename
# middlename対応にshowメソッドもオーバーライド
def show(self) -> None:
print(f'私の名前は{self.lastname}.{self.middlename}.{self.firstname}です!')
if __name__ == '__main__':
fr = Foreigner('太郎', 'ヨーダ', '山田')
fr.show() # 私の名前は山田.ヨーダ.太郎です!
__init__もメソッドの一種であり、supe().__init__ の記法に変わりはない。ただし、一般的に基底クラスの初期化は派生クラスの諸以下に先立って済ませておくべきであり、初期化メソッドでのsupe呼び出しも、メソッド定義の先頭で行うべき。
継承に関わるデコレーター
継承(オーバーライド)に関連して、シグニチャの妥当性、制約を課すための@override, @finalデコレーターが用意されている。
→ 意図しないオーバーライドの誤りを検出しやすくなる。
@overrideデコレーター(Python3.12)
@overrideデコレーター:そのメソッドが基底クラスで定義されたメソッドをオーバーライドしていることを明示的に宣言できる。
class Person:
def __init__(self, firstname: str, lastname: str) -> None:
self.firstname = firstname
self.lastname = lastname
def show(self) -> None:
print(f'私の名前は{self.lastname}{self.firstname}です!')
class BusinessPerson(Person):
def work(self) -> None:
print(f'{self.lastname}{self.firstname}は働いています。')
class EliteBusinessPerson(BusinessPerson):
@override
def work(self) -> None:
print(f'{self.lastname}{self.firstname}はバリバリ働いています。')
if __name__ == '__main__':
bp = EliteBusinessPerson('太郎', '山田')
bp.work() # 山田太郎はバリバリ働いています。
bp.show() # 私の名前は山田太郎です!
@overrideアノテーションは必須ではないが、入力ミスなどによるバグの混在を確実に防ぎ、見た目としてもオーバーライドの意図を明確にすることが出来る。
→ 特別な理由がない限り、明示しておくべき。
@finalデコレーター
@finalデコレーター:継承/オーバーライドを禁止する
継承可能なクラスは、実装/修正にも派生クラスへの影響を配慮しなければならないし、派生クラスの側でも、どのクラス/メソッドであれば「安全に」継承/オーバーライドできるかを選別しなければならない。
→ 無制限に継承/オーバーライドを認めるのは避けるべき。設計時点で、継承/オーバーライドを想定していないクラス/メソッドでは、継承/オーバーライドそのものを禁止すべき(であるし、逆に認めるならドキュメンテーションコメントでもその旨を明記すべき)。
class Person:
def __init__(self, firstname: str, lastname: str) -> None:
self.firstname = firstname
self.lastname = lastname
def show(self) -> None:
print(f'私の名前は{self.lastname}{self.firstname}です!')
class BusinessPerson(Person):
@final
def work(self) -> None:
print(f'{self.lastname}{self.firstname}は働いています。')
@final
class EliteBusinessPerson(BusinessPerson):
def work(self) -> None:
print(f'{self.lastname}{self.firstname}はバリバリ働いています。')
if __name__ == '__main__':
bp = EliteBusinessPerson('太郎', '山田')
bp.work() # 山田太郎はバリバリ働いています。
bp.show() # 私の名前は山田太郎です!
@finalデコレ―タにより、継承/オーバーライドの範囲が明確になるだけでなく、考えるべきことが減るので、コードの可読性も改善する。
→ @overrideデコレーター同様に、許される状況では極力明示するべき。
多重継承とメソッドの検索順序
多重継承において、基底クラスに同名メソッドが存在する場合、いずれを呼び出すかが曖昧になる。
→ 名前の解決ルールを考えなければならない。
まずは簡単な例(菱形/ダイヤモンド継承問題)から考える。
from typing import override
class Top:
def hoge(self) -> None:
print('TopA')
class MiddleA(Top):
@override
def hoge(self) -> None:
print('MiddleA')
class MiddleB(Top):
@override
def hoge(self) -> None:
print('MiddleB')
# MiddleA/Bクラスを多重継承
class Low(MiddleA, MiddleB):
pass
if __name__ == '__main__':
lo = Low()
lo.hoge() # MiddleA
Lowクラスが継承するMiddleA/MiddleBクラスが同名のhogeメソッドを持つことから、lo.hogeがどのメソッドを呼び出すのかが曖昧になる。
Pythonでは、基底クラスとして指定された順にメソッドを検索する。つまり、Low → MiddleA → MiddleB → Topの順で検索される。
以下のような構造を持つ、少し複雑な例を考える。
TopA TopB
^ ^
| \ / |
| \ / |
| \/ |
| /\ |
| / \ |
MiddleA MiddleB
^ ^
\ /
\ /
Low
from typing import override
class TopA:
def hoge(self) -> None:
print('TopA')
class TopB:
def hoge(self) -> None:
print('TopB')
class MiddleA(TopA, TopB):
@override
def hoge(self) -> None:
print('MiddleA')
class MiddleB(TopA, TopB):
@override
def hoge(self) -> None:
print('MiddleB')
class Low(MiddleA, MiddleB):
pass
if __name__ == '__main__':
lo = Low()
lo.hoge() # MiddleA
print([c.__name__ for c in Low.mro()]) # ['Low', 'MiddleA', 'MiddleB', 'TopA', 'TopB', 'object']
例えば、以下のような書き方をするとErrorを吐く。
class MiddleB(TopB, TopA):
@override
def hoge(self) -> None:
print('MiddleB') # TypeError
メソッドの検索ルールを知る
更に以下の例を考える。
Top
^
|
|
|
|
|
MiddleA MiddleB
^ ^
\ /
\ /
Low
実際問題、継承順序の決定ルールは複雑である。上の例でも、直観的に「Low → MiddleA → MiddleB → Top」なのか、あるいは「Low → MiddleA → Top → MiddleB」なのかは判断が難しい(今回は後者、もっと複雑な継承関係であればなおさら判断は困難)。
このような場合も、下のようにmroメソッドを用いることで、現在のクラスに対するメソッド検索順序を確認することが出来る(正確な検索アルゴリズムを開発者が理解していなくともよい)。
print(Low.mro())
ただ、mroメソッドで検索ルートを確認できるという手段以前に、メソッド名が重複するような多重継承自身を避けるべきである。
委譲
継承はコード再利用の代表的アプローチだが、唯一かつ常に最良の手段というわけではないし、むしろ継承を利用すべき場面は相当限られている。
→ 継承を利用するのは基底/派生クラスがis-aの関係を満たしており、かつモジュール(パッケージ)をまたいで継承するならば、そのクラスが「拡張を前提としており、その旨が文書化されている」場合に限定すべき。
派生クラスは基底クラスの実装に依存し、そうである以上内部的な構造を意識しなければならない。基底クラスの実装修正によって、派生クラスが動作しなくなるなど、基底クラスが派生クラスに対して上位であったり、継承構造が複雑になるにつれて、修正コストも高まるため。
継承が不適切な例
is-aの関係を確認するための代表的なアプローチとして、リスコフの置換原則がある。
リスコフの置換原則:派生クラスのインスタンスは、常に基底クラスのインスタンスと置き換え可能である
この原則において、以下のようなMyStackクラスは不適切。
Mystackクラスは、標準のlist型を元にスタック機能を定義している。
from typing import Any, Self
class MyStack(list):
# list#appendをもとにpushメソッドを定義
def push(self, elem: Any) -> Self:
self.append(elem)
return self
# その他の不要なメソッドは無効化
def insert(self, *args, **kwargs) -> None:
raise RuntimeError('Not Support')
def extend(self, *args, **kwargs) -> None:
raise RuntimeError('Not Support')
def remove(self, *args, **kwargs) -> None:
raise RuntimeError('Not Support')
def __setitem__(self, key, value) -> None:
raise RuntimeError('Not Support')
def __delitem__(self, key) -> None:
raise RuntimeError('Not Support')
def sort(self, *args, **kwargs) -> None:
raise RuntimeError('Not Support')
def reverse(self, *args, **kwargs) -> None:
raise RuntimeError('Not Support')
if __name__ == '__main__':
s = MyStack([10, 20, 30])
s.push(40)
print(s.pop()) # 結果: 40
print(s) # 結果: [10, 20, 30]
# 以下は例外が発生する
# s.insert(1, 50)
スタックは後入れ先出しの構造なので、最低限以下のメソッドを定義しておくべきである。
・push:末尾に要素を追加
・pop:末尾から要素を取得
ここでは、list型の append メソッドを元に push メソッドを実装している。popは list のメソッドをそのまま利用可能。
MyStack は list を継承しているが、スタックとして不要な操作(insert, extend, remove, sort, reverse など)をすべて RuntimeError で封じている。
→ これはリスコフの置換原則に反する。s.insert(1, 50) が list として動作しない点で、継承関係として妥当ではない。
委譲による解決
上記のような状況を解決するのが委譲。
委譲:再利用したい機能を持つオブジェクトを、現在のクラスのインスタンス変数として取り込む。
以下の例では、list を内部実装に閉じ込め、スタックとして必要な最小APIだけを公開している(push / pop / peek / __len__ / __bool__ / __iter__ / clear など)。
# delegate_good.py
from typing import Iterable, Iterator, Any
class MyStack:
"""
スタック(LIFO)を委譲で実装。
- 内部に list を保持するが、list のAPIは外部に露出しない
- 必要最小の操作のみ公開する(push/pop/peek/len/bool/iter/clear)
"""
__slots__ = ("_data",)
def __init__(self, init: Iterable[Any] | None = None) -> None:
# 内部実装に list を『委譲』する(継承しない)
# 先頭を底、末尾を頂上(top)として扱う
self._data: list[Any] = list(init) if init is not None else []
# --- 基本操作 ---
def push(self, elem: Any) -> "MyStack":
"""要素を積む(top へ)"""
self._data.append(elem)
return self # メソッドチェーン可能に
def pop(self) -> Any:
"""top を取り出す(空なら IndexError)"""
if not self._data:
raise IndexError("pop from empty stack")
return self._data.pop()
def peek(self) -> Any:
"""top を覗く(空なら IndexError)"""
if not self._data:
raise IndexError("peek from empty stack")
return self._data[-1]
def clear(self) -> "MyStack":
"""全消去"""
self._data.clear()
return self
# --- Python 組み込み連携(最小限) ---
def __len__(self) -> int: # len(stack)
return len(self._data)
def __bool__(self) -> bool: # if stack: ...
return bool(self._data)
def __iter__(self) -> Iterator[Any]:
"""LIFO の直観に合わせ、top→bottom 方向に列挙"""
return reversed(self._data)
def __repr__(self) -> str:
# 表示は top を左に寄せる
body = ", ".join(repr(x) for x in self)
return f"MyStack([{body}])"
# --- 拡張:複数積む(任意) ---
def push_many(self, elems: Iterable[Any]) -> "MyStack":
"""イテラブルの要素を順に push"""
for e in elems:
self.push(e)
return self
if __name__ == "__main__":
s = MyStack([10, 20, 30]) # 30 が top
print(s) # MyStack([30, 20, 10])
s.push(40).push(50)
print(s.peek()) # 50
print(len(s)) # 5
print(list(s)) # [50, 40, 30, 20, 10] ← top→bottom
print(s.pop()) # 50
print(s.pop()) # 40
print(s) # MyStack([30, 20, 10])
# listのAPIは存在しない(封じ込められている)ため AttributeError
# s.insert(1, 999) # => AttributeError: 'MyStack' object has no attribute 'insert'
委譲による改善点は以下
・置換原則を壊さない → list を継承しないので「list としての期待(insert/sort/スライス等)」をユーザーに誤解を与えない。MyStack は スタックとしての契約だけを提供する。
・表現の自由度 → 将来 deque など別構造に変えても外部APIは不変(内部実装の差し替えが容易)。
・インターフェイス最小化 → 余計な操作(insert など)をそもそも公開しない=誤用の余地が減り、テスト対象も明確。
委譲の優れている点は、クラス同士の関係が緩まる点。利用しているのが公開されたメソッドなので、委譲先の内部的な実装に左右される心配がない。
また、属性でインスタンスを保持しているので、クラス同士の関係が固定されない。委譲先を変更するのも自由であるし、複数のクラスに処理をゆだねることも、インスタンス単位に委譲先を切り替えることも可能。
継承をクラス同士の静的な関係とするならば、委譲とはインスタンス同士の動的な関係といってもいい。
ミックスイン
上で、「継承を利用するのは、クラス間にis-aの関係があるときに限定するべき」と述べたが、ミックスイン(Mixin)においては例外。
ミックスイン:再利用可能な機能(メソッド)を束ねたクラスのこと。
→ それ単体で動作することを意図しておらず、他のクラスに継承されることによってのみ動作する。(断片的なクラスといってもいい)
具体的な例を考える。
以下は、show_attrメソッドを定義したLogMixin ミックスインを準備し、Personクラスに組み込んだ例。
class LogMixin:
# 現在のインスタンスの内容を列挙
def show_attr(self) -> None:
for key, value in self.__dict__.items():
print(f'{key}: {value}')
# ミックスインを組み込み
class Peerson(LogMixin):
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
if __name__ == '__main__'
p = Person('鈴木修', 50)
p.show_attr()
# name: 鈴木修
# age: 50
ミックスインといっても、基本的な構文は普通のクラスと変わらない。ただし、あくまで機能を付与するための仕組みなので、インスタンス変数は持たない。
show_attrは、現在のインスタンスの中身を列挙するためのメソッド(__dict__はインスタンス変数を「名前: 値」形式の辞書として返す。)LogMixin自身はインスタンス変数を持たないので、それ単体ではshow_attrメソッドは意味をなさない(それ単体で動作することを意図しない)。
よって、継承によって派生クラスに取り込むようにする。ここではミックスインを継承しているだけだが、もちろん本来の意味での基底クラスを同時に継承しなくてもかまわない。例えば以下。
class Person(MyParent, Logmixin):
# MyParentは、is-aの関係にある本来の基底クラス
Pythonの多重継承は、場合によっては複雑でわかりにくいコードを生み出す原因にもなる。ここで、本来の意味での基底クラスは1つとして、残りは機能だけのミックスインとすることで、コードの見通しを維持しながら、柔軟に機能を拡張することが出来る。
参考文献
[1] 独習Python (2020, 山田祥寛, 翔泳社)
[2] Pythonクイックリファレンス 第4版(2024, Alex, O’Reilly Japan)