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-30

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

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

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

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

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

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

先に比較表

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

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

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

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を小さく作りたい
  • ダックタイピングを保ちながら静的型チェックを使いたい

「Protocolが現在の主流」とは一概に言えない

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

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

そのため、「現代のPythonでは常にProtocol」と決めるのではなく、契約の目的から選びます。

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

ABCでも継承なしのクラスを判定できる

ABCは通常、明示的な継承を使います。ただし、継承していないクラスを仮想サブクラスとして扱う方法もあります。

register() で登録する

from abc import ABC


class Serializer(ABC):
    pass


class ThirdPartySerializer:
    def serialize(self, value: object) -> bytes:
        return str(value).encode()


Serializer.register(ThirdPartySerializer)

assert isinstance(ThirdPartySerializer(), Serializer)

__subclasshook__() で構造を判定する

abc_subclasshook.py
from abc import ABC, abstractmethod
from typing import Any


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

    @classmethod
    def __subclasshook__(cls, subclass: type[Any]):
        if cls is Serializer:
            has_serialize = any(
                "serialize" in base.__dict__ for base in subclass.__mro__
            )
            if has_serialize:
                return True
        return NotImplemented


class ThirdPartySerializer:
    def serialize(self, value: object) -> bytes:
        return str(value).encode()


assert isinstance(ThirdPartySerializer(), Serializer)

__subclasshook__() は判定ロジックを自分で実装できるため柔軟です。ただし、例では serialize という名前の存在しか確認しておらず、引数や戻り値の型までは検査していません。

register()__subclasshook__() で認識された仮想サブクラスは、ABCを実際には継承していません。

ABCの共通実装はMROへ入らず、@abstractmethod の実装強制も受けません。 isinstance()True になることと、基底クラスの機能を継承することは別です。

Protocolでも実行時に isinstance() を使える

通常のProtocolは、isinstance() の第2引数に指定できません。 @runtime_checkable を付けると実行時検証を利用できます。

runtime_protocol.py
from typing import Protocol, runtime_checkable


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


class JsonSerializer:
    def serialize(self, value: object) -> bytes:
        return b"{}"


assert isinstance(JsonSerializer(), Serializer)

ただし、@runtime_checkable は静的型チェックと同じ検証を実行時に行う機能ではありません。属性が存在するかを確認しますが、メソッドの引数や戻り値の型は検査しません。

class IncompatibleSerializer:
    def serialize(self) -> str:
        return "not bytes"


assert isinstance(IncompatibleSerializer(), Serializer)
# Trueになり得るが、静的型チェックではSerializerと互換性がない

Python公式ドキュメントでは、runtime-checkable Protocolに対する isinstance() は通常の isinstance() より遅くなる場合があるため、性能を重視するコードでは hasattr() などの選択肢も検討するよう説明されています。

実行時検証が必要だからといって、すべての境界へ isinstance() を追加する必要はありません。通常のアプリケーションコードでは、静的型チェッカーで契約を確認し、実際の呼び出しで失敗した例外を適切な境界で処理する方法もあります。

実行時検証で選ぶなら何を見るか

ABCとruntime-checkable Protocolのどちらも isinstance() に利用できますが、意味は異なります。

方法 実行時に主に確認するもの 注意点
ABCの明示的なサブクラス 継承関係と抽象メソッドの実装 実装側がABCへ依存する
ABC + register() 開発者が登録したクラス 登録時に構造を自動検証しない
ABC + __subclasshook__() 開発者が実装した判定条件 シグネチャ検証まで行うなら自前実装が必要
Protocol + @runtime_checkable Protocolメンバーの存在 シグネチャや型は検査しない

プラグインをロードする場面で「このフレームワークの正式な実装か」を確認したいなら、ABCの明示的な継承が分かりやすい場合があります。外部オブジェクトが必要な操作を持つかだけを確認したいなら、runtime-checkable Protocolや個別の hasattr() が候補になります。

入力データの値や形式まで保証したい場合は、どちらも十分ではありません。別途バリデーションが必要です。

ジェネリクスは両方で使える

ABCとProtocolは、どちらもジェネリックにできます。ジェネリクスを使いたいかどうかは、両者の選択基準にはなりません。

Python 3.12以降では、型パラメータ構文を使えます。

generic_repository.py
from abc import ABC, abstractmethod
from typing import Protocol


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

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


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

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

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

型パラメータには上限境界や制約も指定できます。ただし、これはABCとProtocolの違いではなく、型変数をどの範囲へ制限するかという別の設計判断です。

判断フロー

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

まとめ

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?