Python | OOP:継承

Python Python
スポンサーリンク

概要(継承は「既存の設計を引き継ぎ、拡張・差し替えする」ための仕組み)

継承は、親クラス(基底クラス)の属性・メソッドを子クラス(派生クラス)が受け継ぎ、必要な部分だけ追加や上書き(オーバーライド)する技術です。初心者がまず押さえるべき核心は、基本構文、オーバーライドの考え方、初期化での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)で解ける問題は合成へ寄せる。この型を体に入れれば、初心者でも継承を「読みやすく、壊れにくく、拡張しやすい」形で使いこなせます。

タイトルとURLをコピーしました