0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PythonのABCとProtocolはどう使い分ける?継承・型チェック・実行時検証を比較する

0
Last updated at Posted at 2026-07-19

Pythonで「このオブジェクトには、このメソッドが必要」と表現したいとき、候補になるのが abc.ABCtyping.Protocol です。

どちらも型の契約を記述できますが、前提とする考え方が異なります。

  • ABC は「この基底クラスを継承しているか」を軸にする
  • Protocol は「必要なメソッドや属性を持っているか」を軸にする

この記事では、両者を同じ題材で比較し、継承、静的型チェック、実行時検証の違いから使い分けを整理します。

この記事のコード例はPython 3.12以降を対象とします。

Protocol 自体はPython 3.8で追加されました。ジェネリクスの節を除けば、ほとんどの例はPython 3.8以降で利用できます。

ABCは継承関係で契約を定義する

ABCはAbstract Base Class(抽象基底クラス)の略です。 ABC を継承し、実装を必須にしたいメソッドへ @abstractmethod を付けます。

abc_example.py
from abc import ABC, abstractmethod


class Serializer(ABC):
    @abstractmethod
    def serialize(self, value: object) -> bytes:
        raise NotImplementedError


class JsonSerializer(Serializer):
    def serialize(self, value: object) -> bytes:
        import json

        return json.dumps(value).encode()

JsonSerializerSerializer を明示的に継承しています。抽象メソッドを実装しないままインスタンス化すると、実行時に TypeError が発生します。

class BrokenSerializer(Serializer):
    pass


BrokenSerializer()
# TypeError: Can't instantiate abstract class BrokenSerializer ...

ABCが向いているのは、継承関係そのものに意味があり、実装側を自分で管理できる場面です。

  • フレームワークが提供する基底クラス
  • プラグインが必ず継承する拡張ポイント
  • 共通実装や状態もサブクラスへ提供したい場合
  • 未実装のサブクラスを実行時にインスタンス化させたくない場合

この性質から、実装を管理でき、継承関係や共通実装を契約に含めたい基盤コードなどの内部契約で有用です。

Protocolはオブジェクトの構造で契約を定義する

Protocolは、必要なメソッドや属性を定義します。実装側はProtocolを継承する必要がありません。

protocol_example.py
from typing import Protocol


class Serializer(Protocol):
    def serialize(self, value: object) -> bytes: ...


class JsonSerializer:
    def serialize(self, value: object) -> bytes:
        import json

        return json.dumps(value).encode()


def save(serializer: Serializer, value: object) -> None:
    payload = serializer.serialize(value)
    print(payload)


save(JsonSerializer(), {"status": "ok"})

JsonSerializerSerializer の間に継承関係はありません。それでも、mypyやPyrightなどの静的型チェッカーは、JsonSerializer が必要なシグネチャを持っているため Serializer と互換性があると判断します。

構造が合わなければ、静的型チェックで検出できます。

class TextSerializer:
    def serialize(self, value: object) -> str:
        return str(value)


save(TextSerializer(), {"status": "ok"})
# 静的型チェックでは、戻り値がbytesではなくstrなのでエラー

Protocolが便利なのは、利用側が必要最小限の契約を定義したい場面です。

  • 外部ライブラリのクラスも同じ関数で受け取りたい
  • 実装側へ特定の基底クラスを継承させたくない
  • テスト用のfakeやstubを小さく作りたい
  • ダックタイピングを保ちながら静的型チェックを使いたい

実装側へ特定の依存を持ち込まず、必要な操作だけを公開APIの境界として示したい場合など、外部契約でも扱いやすい仕組みです。

ジェネリクスで型の関係を保つ

ABCとProtocolは、どちらもジェネリックにできます。ジェネリクスを使いたいかどうかは、両者の選択基準にはなりませんが、抽象化した後も型同士の関係を静的型チェッカーへ伝えるために役立ちます。

入力と出力が同じ型であることを表す

次の first() は、受け取ったリストの要素と同じ型を返します。 list[str] を渡せば戻り値は strlist[int] を渡せば int と推論されます。

def first[T](items: list[T]) -> T:
    return items[0]

型を object にすると戻り値まで object になりますが、型パラメータ T を使えば、具体的な型を固定せずに入力と出力の関係を保てます。

constraintsで許容する型を列挙する

値制約(constraints)は、型パラメータとして許容する型を列挙します。次の例では str または bytes を受け付け、それ以外の型は静的型チェックでエラーになります。

def duplicate[T: (str, bytes)](value: T) -> tuple[T, T]:
    return value, value

boundで共通の上限境界を指定する

上限境界(bound)は、指定したクラスのサブクラスを受け付けます。Protocolをboundに指定した場合は、そのProtocolに適合する型を受け付けます。

from typing import Protocol


class Closable(Protocol):
    def close(self) -> None: ...


def close_and_return[T: Closable](resource: T) -> T:
    resource.close()
    return resource

この例では、close() を持つ具体的な型を受け付け、戻り値も渡したオブジェクトと同じ具体的な型として扱えます。

Python 3.11以前では TypeVar を使う

ここまでの型パラメータ構文はPython 3.12で追加されました。Python 3.11以前をサポートする場合は、TypeVarGeneric を使います。

from abc import ABC, abstractmethod
from typing import Generic, Protocol, TypeVar


T = TypeVar("T")


class RepositoryProtocol(Protocol[T]):
    def get(self, key: str) -> T: ...

    def save(self, value: T) -> None: ...


class RepositoryABC(ABC, Generic[T]):
    @abstractmethod
    def get(self, key: str) -> T:
        raise NotImplementedError

    @abstractmethod
    def save(self, value: T) -> None:
        raise NotImplementedError

constraintsは TypeVar("T", str, bytes)、boundは TypeVar("T", bound=Closable) と記述します。Python 3.12以降では、型パラメータを関数名やクラス名の直後へ書けるため、TypeVar を事前に宣言する必要がありません。

ABCとProtocolの違いから選択基準を整理する

観点 abc.ABC typing.Protocol
基本となる部分型関係 継承・登録による名前的部分型(nominal subtyping) 構造的部分型(structural subtyping)
部分型判定の補足 __subclasshook__() による構造的な実行時判定も可能 メソッドや属性の構造を静的に検査
実装側の明示的な継承 基本的に必要 不要
主なチェック時期 クラス定義・インスタンス化などの実行時 mypyやPyrightなどによる静的解析時
未実装メソッドの実行時強制 @abstractmethod で可能 基本的には行わない
共通実装や状態の提供 向いている 明示的に継承した場合を除き、主目的ではない
既存クラスや外部ライブラリへの適用 継承・登録・フックが必要 構造が一致すれば適用できる
isinstance() 利用できる @runtime_checkable を付けた場合のみ利用できる

資格にたとえるなら、ABCは「指定された資格を持つ人」、Protocolは「資格の有無にかかわらず、必要な作業ができる人」に近い考え方です。

ただし、このたとえだけでABCを「厳密」、Protocolを「緩い」と捉えると正確ではありません。Protocolも静的型チェッカーを通せば、メソッド名だけでなく引数や戻り値まで検査できます。違いは強弱ではなく、契約への参加を継承で宣言するか、構造から判断するかです。

Protocolは、既存クラスへ手を入れずに型の契約を与えられます。依存方向を小さく保ちやすく、ライブラリ境界やテストダブルとの相性もよい仕組みです。

一方、ProtocolはABCを置き換えるために導入されたものではありません。PEP 544では、名前的部分型と構造的部分型にはそれぞれ長所と短所があり、Protocolは既存の名前的部分型を補完するものと説明されています。

選択するときは、APIの公開範囲だけでなく、契約へ参加する方法を基準にします。実装側に基底クラスの継承を求め、共通実装やインスタンス化時の検証も契約に含めるならABCが適しています。実装側へ継承を求めず、必要なメソッドや属性を満たすことを契約とするならProtocolが適しています。

欲しい契約 選択の目安
実装クラスに継承を明示させたい ABC
抽象メソッドの未実装をインスタンス化時に止めたい ABC
共通実装や状態を提供したい ABC
既存クラスを変更せずに受け入れたい Protocol
利用側が必要な機能だけを小さく定義したい Protocol
ダックタイピングを静的に検査したい Protocol

選択フロー

型の契約を定義したい
│
├─ 実装クラスへ継承を明示させたい
│  └─ ABC
│
├─ 共通実装や状態を継承させたい
│  └─ ABC
│
├─ 既存クラスや外部ライブラリも変更せず受け入れたい
│  └─ Protocol
│
├─ 利用側が必要な操作だけを小さく定義したい
│  └─ Protocol
│
└─ 実行時にも検証したい
   │
   ├─ 継承関係と未実装を強制したい
   │  └─ ABC
   │
   └─ メンバーの存在を確認できればよい
      └─ @runtime_checkable または個別の hasattr()

ABCは、明示的に継承したクラスを isinstance() で判定できます。Protocolも @runtime_checkable を付ければ isinstance() に利用できますが、メソッドの引数や戻り値の型までは検査しません。

ABCの register()__subclasshook__() を含む実行時検証の違いは、ABCとProtocolで実行時検証する方法に分けてまとめています。

まとめ

ABCとProtocolは、どちらも契約を表現できます。選択の中心は、契約をどのように成立させたいかです。

  • ABCは、継承関係を明示し、抽象メソッドや共通実装を使いたい場合に向く
  • Protocolは、継承関係を要求せず、構造を静的型チェックしたい場合に向く
  • __subclasshook__()@runtime_checkable を使うと両者は似た動きもできるが、検証内容と継承の効果は異なる
  • どちらが新しいかではなく、名前的な契約と構造的な契約のどちらが必要かで選ぶ

実装側を管理し、フレームワークの一員であることを明示したいならABCが自然です。既存実装を疎結合なまま受け入れたいなら、Protocolが扱いやすくなります。

参考

Python公式ドキュメント

PEP

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?