概要(継承は「既存の設計を引き継ぎ、拡張・差し替えする」ための仕組み)
継承は、親クラス(基底クラス)の属性・メソッドを子クラス(派生クラス)が受け継ぎ、必要な部分だけ追加や上書き(オーバーライド)する技術です。初心者がまず押さえるべき核心は、基本構文、オーバーライドの考え方、初期化でのsuper().initの使い方、そして多重継承で混乱しないためのMRO(メソッド解決順序)です。無理に継承へ寄らず、合成(has-a)との使い分けを軸にすると壊れにくくなります。
継承の基本(構文・オーバーライド・「is-a」の判断)
最小構文と動作の確認
class Animal:
def __init__(self, name: str):
self.name = name
def speak(self) -> str:
return "..."
class Dog(Animal): # Animalを継承
pass
d = Dog("ポチ")
print(d.name) # 親で初期化した属性を使える
print(d.speak()) # 親メソッドもそのまま使える("...")
Python子クラスは親の属性とメソッドをそのまま使えます。これが“引き継ぎ”の基本形です。
メソッドのオーバーライド(振る舞いを差し替える)
class Dog(Animal):
def speak(self) -> str:
return "ワン!"
d = Dog("ポチ")
print(d.speak()) # "ワン!"(親を子が上書き)
Python「既存の形は保ちたいが振る舞いを変えたい」場合、オーバーライドが最もシンプルで強力です。
「is-a」で継承を選ぶ(合成との線引き)
DogはAnimalである(is-a)ため継承が自然ですが、「ログ機能を足したい」「課金機能を持たせたい」などは“持っている”(has-a)関係です。そういう場合は別クラスを持たせる合成が壊れにくく、テストもしやすくなります。
初期化とsuper(親の準備を呼び、子の拡張を足す)
親の初期化を忘れない(super().init)
class Animal:
def __init__(self, name: str):
self.name = name
class Dog(Animal):
def __init__(self, name: str, breed: str):
super().__init__(name) # 親の初期化を呼ぶ
self.breed = breed
Python親が定めた前提(必須属性・バリデーション)を保つため、子でもsuper().initを呼ぶのが基本です。
親の契約を守りつつ、子で属性を追加する
親が「nameは必須」と決めているなら、子もそれを満たした上で独自属性(breedなど)を足します。親の契約を破る設計は、下流でのバグ増幅に直結します。
initを軽く保つ(I/Oは明示メソッドへ)
子の初期化でネットワークやファイルI/Oを走らせると、生成だけで重く壊れやすくなります。接続や読込はconnect/loadなどのメソッドに切り出し、初期化は軽量に保ちます。
多重継承とMRO(メソッド解決の順序を理解して安全に使う)
MROの確認(どの順で探されるか)
Pythonは「左から右へ、浅い方から深い方へ」の順で親を辿るMROを持ちます。確認はmro()で可能です。
class A: pass
class B: pass
class C(A, B): pass
print(C.mro()) # [<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>]
Pythonこの順序でメソッドや属性が解決されるため、意図しない上書きや衝突を避ける設計ができます。
協調的なsuper(多重継承での連携)
多重継承では、各クラスのメソッドがsuperを“協調的に”呼ぶ前提で設計すると安全です。
class Base:
def setup(self):
print("Base")
class MixinA(Base):
def setup(self):
super().setup()
print("A")
class MixinB(Base):
def setup(self):
super().setup()
print("B")
class Final(MixinA, MixinB):
def setup(self):
super().setup()
print("Final")
Final().setup()
# 出力例(MRO順で呼ばれる)
# Base
# B
# A
# Final
Python各クラスがsuper().setup()を呼ぶことで、MROに従い重複なく一周できます。
ミックスインの設計(小さな能力を足す)
ミックスインは「小さな追加機能(ログ、リトライ、キャッシュ)」をクラスへ付与するための軽量な親クラスです。状態を持ち過ぎず、単機能で“足せる”形にしておくと、組み合わせが壊れにくくなります。
Web / APIの実例(クライアント・例外・モデルでの継承)
基底クライアントを継承して系統別設定を持つ
import requests
class BaseClient:
base_url = "https://api.example.com"
def __init__(self, api_key: str, timeout: float = 5.0):
self.timeout = timeout
self.session = requests.Session()
self.session.headers.update({"Authorization": f"Bearer {api_key}"})
def get(self, path: str) -> dict:
r = self.session.get(f"{self.base_url}{path}", timeout=self.timeout)
r.raise_for_status()
return r.json()
class EUClient(BaseClient):
base_url = "https://eu.api.example.com"
class USClient(BaseClient):
base_url = "https://us.api.example.com"
Python共通機能(セッション・ヘッダー)は親で持ち、拠点ごとのベースURLは子で上書きすると、再利用性と可読性が上がります。
例外型の継承で意味を表す(ドメイン例外)
class AppError(Exception): ...
class ConfigError(AppError): ...
class ServiceError(AppError): ...
def read_config(path: str) -> dict:
import json
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except (OSError, json.JSONDecodeError) as e:
raise ConfigError(f"設定読込失敗: {path}") from e
Python例外の継承ツリーを作ると、上位で「アプリ由来の失敗」をひとまとめに扱えます。
データモデルの継承(共通項目+拡張)
class BaseModel:
def to_dict(self) -> dict: raise NotImplementedError
class User(BaseModel):
def __init__(self, uid: str, name: str | None = None):
self.uid = uid; self.name = name or "unknown"
def to_dict(self) -> dict:
return {"id": self.uid, "name": self.name}
class Admin(User):
def __init__(self, uid: str, name: str | None = None, role: str = "admin"):
super().__init__(uid, name)
self.role = role
def to_dict(self) -> dict:
d = super().to_dict()
d["role"] = self.role
return d
Python共通の表現(to_dict)を親で定め、子は差分だけ足すと拡張が容易になります。
よくある落とし穴と対策(階層肥大・契約破り・diamond問題)
階層が深くなり過ぎる
「親→子→孫」と増やしすぎると変更が伝播し、理解も困難になります。まず合成で解決できないか検討し、継承段数は必要最小限に抑えます。
親の契約を破るオーバーライド
引数の意味や戻り値の型を勝手に変えると、置き換え可能性(Liskovの原則)が破れてバグの温床になります。子は親の契約を守り、拡張は追加引数のデフォルトや返却辞書へキー追加で表現します。
ダイヤモンド継承の衝突
A→B,C→Dのような“菱形”で同じメソッドが複数の親にあると、解決順序の理解が必要です。協調的super、薄いミックスイン、設計の簡素化で回避しましょう。MROを常に確認できるようにしておくと安心です。
例題(ログ+リトライをミックスインで付与する安全なクライアント)
ミックスインで能力を重ねる
import time, requests
class LoggingMixin:
def log(self, msg: str): print(msg)
class RetryMixin:
def request_with_retry(self, fn, tries=3, base=0.5):
for i in range(tries):
try:
return fn()
except requests.RequestException as e:
self.log(f"retry {i+1}: {e}")
time.sleep(base * (2 ** i))
raise
class Client(BaseClient, LoggingMixin, RetryMixin):
def get_safe(self, path: str) -> dict:
def call():
r = self.session.get(f"{self.base_url}{path}", timeout=self.timeout)
r.raise_for_status()
return r.json()
return self.request_with_retry(call)
Python共通の枠(BaseClient)に、ログとリトライの“小さな能力”をミックスインで付けると、拡張が軽く、組み合わせも柔軟になります。
まとめ(「親の契約を守り、子で差分だけ足す」。合成との使い分けが肝)
継承は、親の属性・メソッドを受け継ぎ、必要な振る舞いをオーバーライドや追加で表現するための中核技術です。初期化ではsuper().initで親の準備を呼び、契約(引数・戻り値)を守る。多重継承はMROと協調的superを理解し、ミックスインを“薄く小さく”設計する。安易に階層を増やさず、合成(has-a)で解ける問題は合成へ寄せる。この型を体に入れれば、初心者でも継承を「読みやすく、壊れにくく、拡張しやすい」形で使いこなせます。
