Python | OOP:抽象クラス(ABC)

Python Python
スポンサーリンク

概要(抽象クラスは「必ず実装してほしい共通の約束」を形にする設計図)

抽象クラス(ABC: Abstract Base Class)は、直接インスタンス化できない「設計図」です。サブクラスが必ず実装すべきメソッド(抽象メソッド)を定め、実装漏れを防ぎます。Pythonでは標準のabcモジュールを使い、ABCを継承し@abstractmethodで必須メソッドを宣言します。ポイントは「インターフェースの統一」「初期化や部分実装の共通化」「テスト容易性」です。


基本構文(ABCと@abstractmethodの使い方)

最小例(“鳴く”を強制する動物の設計図)

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def speak(self) -> str:
        """動物は必ず鳴ける(実装必須)"""
        pass

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

d = Dog()
print(d.speak())  # ワン!
# Animal() は TypeError(抽象メソッド未実装のため)
Python

抽象クラスは直接インスタンス化できません。必須メソッドの未実装を、実行時に確実に検出できるのが利点です。

部分実装+必須拡張(共通処理は親、差分は子)

from abc import ABC, abstractmethod

class Formatter(ABC):
    def header(self, title: str) -> str:
        return f"=== {title} ==="  # 共通の前処理(実装済み)

    @abstractmethod
    def body(self, data: dict) -> str:
        """本文の作り方は子が決める"""
        pass

    def render(self, title: str, data: dict) -> str:
        return self.header(title) + "\n" + self.body(data)

class JsonFormatter(Formatter):
    def body(self, data: dict) -> str:
        import json
        return json.dumps(data, ensure_ascii=False, indent=2)

print(JsonFormatter().render("ユーザー", {"id": 1, "name": "太郎"}))
Python

共通の枠組みは抽象クラスで提供し、差分だけを抽象メソッドとして子に強制すると、拡張しやすく壊れにくくなります。


初期化・プロパティ・テンプレートメソッド(“契約”を強化する設計)

initで前提を固定し、抽象メソッドへ渡す

from abc import ABC, abstractmethod

class Client(ABC):
    def __init__(self, base_url: str, timeout: float = 5.0):
        if not base_url.startswith("https://"):
            raise ValueError("HTTPSのみ許可")
        self._base_url = base_url
        self._timeout = timeout

    @property
    def base_url(self) -> str:
        return self._base_url

    @property
    def timeout(self) -> float:
        return self._timeout

    @abstractmethod
    def get_user(self, uid: str) -> dict:
        """各クライアントが必ず持つ操作"""
        pass

class RequestsClient(Client):
    def get_user(self, uid: str) -> dict:
        import requests
        r = requests.get(f"{self.base_url}/users/{uid}", timeout=self.timeout)
        r.raise_for_status()
        data = r.json()
        return {"id": str(data.get("id", "")), "name": data.get("name") or "unknown"}
Python

抽象クラスで「安全な前提」を整え、公開プロパティで渡すと、子の実装が一貫して正しくなります。

テンプレートメソッドで流れを固定し、差分のみ抽象化

from abc import ABC, abstractmethod

class Handler(ABC):
    def handle(self, payload: dict) -> dict:
        self._validate(payload)
        result = self._process(payload)   # ← 抽象メソッドに差し替え
        return self._decorate(result)

    def _validate(self, payload: dict) -> None:
        if "id" not in payload:
            raise ValueError("id必須")

    @abstractmethod
    def _process(self, payload: dict) -> dict:
        pass

    def _decorate(self, result: dict) -> dict:
        result["ok"] = True
        return result

class UserHandler(Handler):
    def _process(self, payload: dict) -> dict:
        return {"id": payload["id"], "name": payload.get("name") or "unknown"}
Python

処理の“型”を抽象クラスで固定し、変わる部分だけ抽象メソッドにすると、保守とテストが楽になります。


応用(抽象プロパティ・部分抽象・ミックスインとの併用)

抽象プロパティ(値の契約も強制)

from abc import ABC, abstractmethod

class Shape(ABC):
    @property
    @abstractmethod
    def area(self) -> float:
        """面積は必須の読み取りプロパティ"""
        pass

class Rectangle(Shape):
    def __init__(self, w: float, h: float):
        if w <= 0 or h <= 0:
            raise ValueError("正の数")
        self._w = w; self._h = h

    @property
    def area(self) -> float:
        return self._w * self._h
Python

プロパティにも抽象を適用でき、読み取り専用の“契約”を子へ強制できます。

部分抽象(デフォルト実装も用意して必要箇所だけ強制)

from abc import ABC, abstractmethod

class Logger(ABC):
    def log(self, msg: str) -> None:
        print(msg)  # デフォルトの簡易実装

    @abstractmethod
    def format(self, level: str, msg: str) -> str:
        pass

class JsonLogger(Logger):
    def format(self, level: str, msg: str) -> str:
        import json
        return json.dumps({"level": level, "msg": msg}, ensure_ascii=False)
Python

「全部抽象」にせず、共通の便利実装を持たせると利用のしきいが下がります。

ミックスインと組み合わせて“小さく足す”

ミックスインは“薄い能力”の追加用。抽象クラスで枠を定め、ミックスインでログ・リトライなどを小さく重ねると拡張が楽です。


よくある落とし穴と回避(過剰継承・契約破り・プロトコルとの使い分け)

階層を深くしすぎない

抽象クラスを積みすぎると理解・変更が難しくなります。まずは合成(has-a)で解けないか検討し、抽象は「本当に必要な枠」だけにします。

契約(シグネチャ)を勝手に変えない

抽象メソッドの引数や戻り値の意味を子で変えると互換性が壊れます。必要な拡張は、追加引数にデフォルトを付ける、戻り値に情報を“追加”するなどで整合を保ちます。

Protocol(構造的部分型)との使い分け

typing.Protocolは「メソッドを持っていればOK」という“構造的なインターフェース”。継承に依らず柔軟です。強制力と階層管理が必要ならABC、柔軟な受け入れが必要ならProtocolを選びます。

from typing import Protocol

class Speaker(Protocol):
    def speak(self) -> str: ...

def greet(x: Speaker) -> str:
    return x.speak()
Python

例題(Webサービスの“枠”をABCで固め、具体実装を差し替える)

サービスの共通枠+具象の差し替え

from abc import ABC, abstractmethod
import requests

class UserService(ABC):
    def __init__(self, base_url: str, timeout: float = 5.0):
        if not base_url.startswith("https://"):
            raise ValueError("HTTPSのみ許可")
        self._base_url = base_url
        self._timeout = timeout

    def get(self, path: str) -> dict:
        r = requests.get(f"{self._base_url}{path}", timeout=self._timeout)
        r.raise_for_status()
        return r.json()

    @abstractmethod
    def profile(self, uid: str) -> dict:
        """取得したデータの整形仕様はサブクラスが決める"""
        pass

class SimpleUserService(UserService):
    def profile(self, uid: str) -> dict:
        data = self.get(f"/users/{uid}")
        return {"id": str(data.get("id", "")), "name": data.get("name") or "unknown"}

class AdminUserService(UserService):
    def profile(self, uid: str) -> dict:
        data = self.get(f"/admins/{uid}")
        return {"id": str(data.get("id", "")), "name": data.get("name") or "unknown", "role": "admin"}

svc = SimpleUserService("https://api.example.com")
print(svc.profile("123"))
Python

共通のI/Oは抽象クラスで提供し、“どのエンドポイントをどう整形するか”だけを具象で差し替えると、変更が安全かつ局所化されます。


まとめ(抽象クラスは“守るべき約束”をコードに刻む)

抽象クラス(ABC)は、サブクラスが必ず実装すべきインターフェースを強制し、設計を一貫させます。共通処理は抽象クラスに集約し、差分だけを抽象メソッドに。初期化で前提を固定し、テンプレートメソッドで流れを守る。過剰な階層を避け、契約の互換性を維持し、必要に応じてProtocolの柔軟性も活用する。この型を体に入れれば、初心者でも「拡張に強く、壊れにくい」OOP設計を自然に構築できます。

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