<一覧に戻る

インターフェースとしてのプロトコル(Python 3.8以降)

「インターフェース」と聞くと、難しそう…と感じるかもしれません。

Pythonにはプロトコル(Protocol)という仕組みがあり、Java の interface のような堅苦しさはなく、ふだんの Python コードに自然に扱えます。しかも、型ヒントと一緒に使うことで、実行前にミスを発見できる静的型チェックにも対応できます。

今回は、インターフェースとしてのプロトコルについて解説します。

プロトコルとは何か

Pythonにはダックタイピングという考え方があります。 「アヒルみたいに鳴いて歩けば、それはアヒルとして扱っていい」という柔軟な考え方です。

プロトコルは、この柔軟さを保ちながらも、「このオブジェクトは少なくともこのメソッド(や属性)を持っていますよ」という約束を、型ヒントとして明確に表現する仕組みです。

特定のクラスを継承しているかどうかではなく、必要なメソッドや属性が揃っているかで判断します。だから、既存のクラスを作り直さなくても、振る舞いが合っていればプロトコルを満たします。

説明だけではよく分からないと思うので、実際のコードで確認していきましょう。

基本の書き方

Python 3.8 以降では、標準の typing モジュールから Protocol を読み込んで使えます。以下のように「こういうメソッドがあるはず」という“型の約束”を記述します。

from typing import Protocol

class MyProtocol(Protocol):
    def method_a(self) -> str:
        ...

    def method_b(self, value: int) -> None:
        ...

... は「ここでは実装しません」という意味です。プロトコル自体は実装を持たず、あくまで「こう振る舞うもの」という型の説明書になります。

実際にこのプロトコルを満たすクラスを作るときは、method_a と method_b を定義しなければなりません。

プロトコルのポイントは「明示的に継承しなくても、型が合っていれば自動的に適合する」という点です。 つまり、MyProtocol を引数として受け取る関数に、method_a と method_b を実装している別のクラスを渡せば、それも正しく扱えるようになります。

サンプルコード:描画できるもの「Drawable」

次の例では、draw() できるものを「Drawable」というプロトコルで表し、丸や四角がその“インターフェース”に従う様子を見てみます。

from typing import Protocol

# プロトコルの定義
class Drawable(Protocol):
    def draw(self) -> None:
        ...

# プロトコルに従うクラス(継承は不要)
class Circle:
    def draw(self) -> None:
        print("Drawing a Circle")

class Square:
    def draw(self) -> None:
        print("Drawing a Square")

# プロトコルを受け取る関数
def render(shape: Drawable) -> None:
    shape.draw()

# 実行
circle = Circle()
square = Square()

render(circle)  # 出力: Drawing a Circle
render(square)  # 出力: Drawing a Square

ここでは CircleSquareDrawable を継承していません。それでも draw() メソッドを持っていれば、render() 関数は問題なく動きます。これがプロトコルの良さで、既存コードにも後から型の意味を与えられます。「インターフェースを満たすかどうかは、継承ではなく“構造”(持っているメソッドや属性)で決まる」という点が最大の特徴です。

実行時にも確かめたい? runtime_checkable で確認しよう

静的型チェッカー(たとえば mypy)を使うと、実行前に「ちゃんと draw() を持っているか」を検査できます。では、実行時に isinstance(obj, Drawable) のように確認したいときは? そんな場合は @runtime_checkable を使います。

from typing import Protocol, runtime_checkable

@runtime_checkable
class Drawable(Protocol):
    def draw(self) -> None:
        ...

class Circle:
    def draw(self) -> None:
        print("Drawing a Circle")

class Triangle:
    # draw を実装していない
    pass

c = Circle()
t = Triangle()

print(isinstance(c, Drawable))  # True
print(isinstance(t, Drawable))  # False

@runtime_checkable を付けると、isinstance で「必要な属性が存在するか」を実行時にざっくり確認できます(関数の引数の型や戻り値までは厳密にチェックしません。厳密さは静的型チェッカーの役目です)。

メソッドだけじゃない:属性やプロパティもプロトコルにできる

プロトコルはメソッドの有無だけでなく、属性やプロパティの存在も表せます。インスタンスが name という文字列属性を持つことを約束させたいときは、こんなふうに書けます。

from typing import Protocol

class Named(Protocol):
    name: str

def greet(x: Named) -> None:
    print(f"Hello, {x.name}!")

class User:
    def __init__(self, name: str) -> None:
        self.name = name

class Bot:
    name = "HelperBot"

greet(User("Alice"))  # Hello, Alice!
greet(Bot())          # Hello, HelperBot!

UserBotNamed を継承していませんが、どちらも name: str を持っているため、greet() に渡せます。まさに「必要なものが揃っていれば OK」という考え方です。

抽象クラス(ABC)との違いは?

「抽象クラス(ABC)と何が違うの?」と感じた方もいるでしょう。抽象クラスは“名目上の型”で、クラスを継承して実装を埋める前提です。対してプロトコルは“構造的な型”で、継承しなくても必要なメソッドや属性が揃っていれば合格です。

  • 抽象クラスは「この家系に属しているか」を見ます。
  • プロトコルは「必要な部品を持っているか」を見ます。

既存コードに型の意味を後付けしたい、ライブラリ間のゆるい結合を保ちたい、といった場面ではプロトコルが特に便利です。一方で、実装のテンプレートを配布したい、大枠の共通処理を持たせたい場合は抽象クラスが向いています。

どんなときにプロトコルが役立つ?

プロトコルは以下のような状況で役に立ちます。

  • ライブラリやモジュール間の結合をゆるく保ちたいとき
  • 既存クラスを壊さずに「こういうインターフェースとして扱う」と明示したいとき
  • 型ヒントを充実させて、静的型チェックでバグを早期発見したいとき
  • ポリモーフィズム(多態性)を、Python らしい書き味のまま安全に活用したいとき

「ルールで縛る」のではなく、「必要十分な約束をはっきり書く」ことで、読みやすさと保守性がぐっと上がります。

まとめ

ここまで学習した内容をまとめます。

  • プロトコルは「必要なメソッドや属性を満たしているか」で判断する、Python らしいインターフェースの表現方法です。
  • 継承しなくても、構造が合っていれば“そのものとして扱える”ため、既存コードにも導入しやすく、設計の自由度が上がります。
  • 型ヒントと組み合わせれば、mypy などで静的型チェックができ、ミスを実行前に発見できます。

「抽象クラスほど重くはしたくないけれど、インターフェースは明示したい」。そんなときにプロトコルは最適です。次は「継承によるコードの再利用」に進み、オブジェクト指向らしい設計をさらに深めていきましょう。

出力結果: