Python | OOP:オーバーライド

Python Python
スポンサーリンク

概要(オーバーライドは「親の振る舞いを、子で意図通りに差し替える」技術)

オーバーライドは、継承した親クラスのメソッドを子クラスで同じ名前で再定義して、振る舞いを変更・拡張する仕組みです。共通部分は親に任せ、違う部分だけ子で変えるのが基本。重要なのは、親の「契約」(引数や戻り値の意味)を守ること、必要に応じてsuper()で親の処理を呼び継ぐこと、そして過剰な上書きで壊れないよう責務を整理することです。


基本構文と考え方(同名メソッドの再定義と互換性の確保)

最小のオーバーライド

class Animal:
    def speak(self) -> str:
        return "..."

class Dog(Animal):
    def speak(self) -> str:
        return "ワン!"

print(Animal().speak())  # ...
print(Dog().speak())     # ワン!
Python

親のメソッド名と同じ名前で定義すると、子のインスタンスでは子のメソッドが優先されます。

互換性(親の契約)を守る

親が引数や戻り値の意味を定めているなら、子でもそれを壊さないようにします。必要な追加は、デフォルト引数や付加情報の追加で表現します。

class PriceFormatter:
    def format(self, amount: float) -> str:
        return f"{amount:.2f}"

class JpyPriceFormatter(PriceFormatter):
    def format(self, amount: float) -> str:
        base = super().format(amount)  # 親の仕様(丸め方)を活かす
        return f"{base} JPY"
Python

super()で「親の処理を活かして差分を足す」

前処理→親→後処理の型

class BaseService:
    def process(self, payload: dict) -> dict:
        if "id" not in payload:
            raise ValueError("id必須")
        return {"id": payload["id"], "ok": True}

class LoggingService(BaseService):
    def process(self, payload: dict) -> dict:
        print("[start]", payload)    # 前処理(ログ)
        base = super().process(payload)  # 親の検証・基本整形
        base["logged"] = True        # 後処理(付加情報)
        print("[done]", base)
        return base
Python

親の「正しさ」を活かしつつ、子で付加機能を重ねます。この順序で書くと壊れにくく読みやすいです。

初期化でも親の準備を呼ぶ

class ClientBase:
    def __init__(self, api_key: str, timeout: float = 5.0):
        import requests
        self.timeout = timeout
        self.session = requests.Session()
        self.session.headers.update({"Authorization": f"Bearer {api_key}"})

class EUClient(ClientBase):
    def __init__(self, api_key: str, timeout: float = 7.0):
        super().__init__(api_key, timeout)  # 親のセッション準備
        self.region = "EU"
Python

親が用意する前提(セッション・バリデーションなど)は必ずsuper().initで呼び継いでください。


Web / APIの実例(クライアント・例外・モデルでのオーバーライド)

APIクライアントのエンドポイント差し替え

import requests

class BaseClient:
    base_url = "https://api.example.com"
    def __init__(self, key: str, timeout: float = 5.0):
        self.timeout = timeout
        self.session = requests.Session()
        self.session.headers.update({"Authorization": f"Bearer {key}"})
    def get_user(self, uid: str) -> dict:
        r = self.session.get(f"{self.base_url}/users/{uid}", timeout=self.timeout)
        r.raise_for_status()
        return r.json()

class EUClient(BaseClient):
    base_url = "https://eu.api.example.com"   # クラス変数の上書き(設定のオーバーライド)
Python

共通の処理は親に集約し、拠点別の違いはクラス属性やメソッドをオーバーライドして切り替えます。

例外の意味を統一する「翻訳」オーバーライド

class AppError(Exception): ...
class ServiceError(AppError): ...

class UserService:
    def __init__(self, client: BaseClient):
        self.client = client
    def profile(self, uid: str) -> dict:
        try:
            data = self.client.get_user(uid)
            return {"id": data.get("id", ""), "name": data.get("name") or "unknown"}
        except Exception as e:
            raise ServiceError("ユーザー取得失敗") from e
Python

外部の生例外は、ドメイン例外へ置き換えて上位へ伝えると扱いが一貫します(これも広義の“振る舞いの上書き”)。

モデルの出力表現を拡張

class UserModel:
    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 AdminModel(UserModel):
    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

親の出力を尊重し、差分だけを加えるのが安全です。


特殊メソッドのオーバーライド(表示・比較・呼び出し)

str/reprで表示を整える

class Price:
    def __init__(self, amount: float, currency: str = "JPY"):
        self.amount = amount; self.currency = currency
    def __str__(self) -> str:         # 人向け表示
        return f"{self.amount:.2f} {self.currency}"
    def __repr__(self) -> str:        # 開発者向け詳細
        return f"Price(amount={self.amount!r}, currency={self.currency!r})"
Python

eqで意味のある比較にする

class Price:
    def __init__(self, amount: float, currency: str = "JPY"):
        self.amount = amount; self.currency = currency
    def __eq__(self, other: object) -> bool:
        return isinstance(other, Price) and (self.amount, self.currency) == (other.amount, other.currency)
Python

比較の意味を定義すると、テストや集合操作が扱いやすくなります。


重要ポイントの深掘り(責務の線引き・テスト・過剰な上書きの回避)

いつオーバーライドし、いつ合成にするか

  • 「is-a(〜である)」ならオーバーライドで振る舞いを差し替える。
  • 「has-a(〜を持つ)」なら別クラスを持たせる合成を優先。ログ・キャッシュ・リトライなどはミックスインか合成が壊れにくい。

テストしやすい形に分解する

  • I/O(外部通信)とロジック(整形・検証)を分け、ロジック側のオーバーライドはモック無しでテスト可能にする。
  • 親の契約を固定したテスト(共通ケース)と、子の差分テスト(拡張部分)を用意すると安心。

過剰な上書きはバグの温床

  • 引数の意味や戻り値の型を変える上書きは避ける(互換性を壊す)。
  • 親を完全に無視するより、super()で既存の正しさを活かす方が安全。

例題(割引計算をオーバーライドで柔軟に切り替える)

親に共通ロジック、子で差分だけ上書き

class Discount:
    def rate(self) -> float:
        return 0.05  # 基本割引5%
    def apply(self, price: float) -> float:
        return price * (1 - self.rate())

class PremiumDiscount(Discount):
    def rate(self) -> float:
        return 0.15  # プレミアムは15%

class CampaignDiscount(Discount):
    def __init__(self, extra: float = 0.05):
        self.extra = extra
    def rate(self) -> float:
        return min(0.05 + self.extra, 0.3)  # 上限30%

print(Discount().apply(1000))         # 950.0
print(PremiumDiscount().apply(1000))  # 850.0
print(CampaignDiscount(0.1).apply(1000))  # 850.0
Python

「共通の計算式は親」「差分の割引率は子」で分けると、拡張・テストが簡単になります。


まとめ(親の仕様を尊重し、子で“差分だけ”を洗練させる)

オーバーライドは、親クラスのメソッドを子で再定義して振る舞いを変える技術です。親の契約(引数・戻り値・前提)を守り、必要ならsuper()で親の処理を活かしながら差分を足す。Web/APIではクライアント設定・例外翻訳・モデル出力などで効果的に使える。合成との使い分けを意識し、過剰な上書きで互換性を壊さない。これを型として徹底すれば、初心者でも「読みやすく、壊れにくく、拡張しやすい」設計が自然にできるようになります。

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