構造的部分型を実現する、PythonのProtocolについて調べました

2025-07-07

はじめに

どうも、こんにちは。Belong で Software Engineer として働いている suzu です。
業務の一環で Python を使う機会があったので「ロバスト Python」1を読んでいたところ、Protocol という機能が紹介されていたので詳しく調べてみました。

※ Belong には業務に関連する技術書を、会社書籍として購入できる制度があります!

Protocol とは

Protocol は Python 3.8 から導入された機能で、Structural Subtyping を実現するためのものです。
根底の仕様は、PEP 544 – Protocols: Structural subtyping (static duck typing) 2に基づいています。

from typing import Protocol

class Animal(Protocol):
    def talk(self) -> None:
       print("Animal talks")

class Dog:
    def talk(self) -> None:
        print("Bow wow")

def make_animal_talk(animal: Animal) -> None:
    animal.talk()

make_animal_talk(Dog())  # Bow wow

Structural Subtyping とは

Structural Subtyping (構造的サブタイピング)は、クラスの継承関係に依存せず、同じメソッドやプロパティを持っているかどうかでサブクラスかどうかを判定する仕組みです。 TypeScript や Go などの言語で見られる Duck Typing に似た概念です。 それ以外の言語で見られる、型の「名前」や「明示的な宣言」によってサブクラス関係を定めるものを、Nominal Subtyping と言います。

また、関連する概念として、型の互換性や等価性の判断を行う文脈で Structural Typing / Nominal Typing という概念があります。

Go のインターフェースとの違い

Go のインターフェースと似ていますが、Go はコンパイル時・実行時の両方でインターフェースをチェックする一方で、Python の Protocol は「静的な型チェックに限定される点・ メソッドだけでなく、プロパティも Protocol として定義可能という点」が大きく異なりそうです。

ABC との違い

Python には、抽象基底クラス (ABC) という機能もありますが、 Protocol は ABC と異なり、Structural Subtyping であるため、明示的な継承を必要としません。 以下のような違いがあります。

typing.Protocolabc.ABC
方式構造的サブタイピング公称的サブタイピング
関係性構造が一致すれば OK明示的に継承する必要がある
適合方法必要なメソッドを実装するだけ@abstractmethod を実装し、クラスで継承
主な用途静的型チェック実行時の型チェック、API の強制

Protocol の使い方

定義

Protocol を定義するには、typing モジュールから Protocol クラスをインポートし、継承してクラスを定義します。

Protocol として表現したいメソッドやプロパティを定義することで、Protocol を作成できます。

class Animal(Protocol):
    name: str  # プロパティの定義
    def talk(self) -> None:  # メソッドの定義
        pass

class Dog:
    name: str = "Dog"
    def talk(self) -> None:
        print("Bow wow")

Protocol は、定義したメソッド、プロパティのことを Protocol members と呼びます。 Protocol members が欠けると、そのクラスは Protocol を実装していないとみなされます。

実装

Protocol を実装するには、同じメソッド名と引数を持つクラスを定義するだけで OK です。

from typing import Protocol

class Animal(Protocol):
    name: str
    def talk(self) -> None:
        pass

class Dog:
    name: str = "Dog"
    def talk(self) -> None:
        print("Bow wow")

make_animal_talk(Dog())  # Bow wow

逆に要素が足りないと、そのクラスは Protocol を実装していないとみなされます。

以下に例を示します。

QuietDogは、メソッド talk が定義されていないため、Animal Protocol を実装していないとみなされます。
NamelessDogは、Protocol members の name プロパティが欠けているため、Animal Protocol を実装していないとみなされます。

# NamelessDog doesn't implement Animal.
class NamelessDog:
    def talk(self) -> None:
        print("Bow wow")

# QuietDog doesn't implement Animal.
class QuietDog:
    name: str = "Dog"

def make_animal_talk(animal: Animal) -> None:
    animal.talk()

make_animal_talk(NamelessDog()) # Bow wow (But linter make warning.)
make_animal_talk(QuietDog())  # AttributeError: 'QuietDog' object has no attribute 'talk'

この場合、静的解析がどうなるかみてみましょう。 NamelessDogQuietDog は、Animal Protocol を実装していないため、make_animal_talk 関数に渡そうとすると Linter で警告が出ます。

しかし、実際の実行では少し違います。 NamelessDog は、talk メソッドを持っているため動作でき、実行時には問題なく動作します。 一方で、QuietDogtalk メソッドを持っていないため、実行時に AttributeError が発生します。

なので、静的解析上では厳密にチェックされますが、実際の実行では Duck Typing に基づいた動作をとります。

Protocol の継承

Protocol は継承でき、その場合は継承元からデフォルト値を継承します。

class Animal(Protocol):
    name: str = "Default Name"
    def talk(self) -> None:
        pass

class SampleAnimal(Animal):
    pass

print(SampleAnimal().name)  # Default Name

複数の Protocol の実装

また、実装を満たせば 1 つのクラスに対し、複数の Protocol を実装することも可能です。 下記の例を見てみましょう。

from typing import Protocol

class Talker(Protocol):
    name: str
    def talk(self) -> None:
        pass

class Walker(Protocol):
    def walk(self) -> None:
        pass

class Dog:
    name: str = "Dog"
    def talk(self) -> None:
        print("Bow wow")
    def walk(self) -> None:
        print("Dog walks")

def make_talker(talker: Talker) -> None:
    talker.talk()
def make_walker(walker: Walker) -> None:
    walker.walk()

make_talker(Dog())  # Bow wow
make_walker(Dog())  # Dog walks

Dog クラスは Talker のもつ talk メソッド (及び name プロパティ) と Walker のもつ walk メソッドを実装しています。 したがって、Dog クラスは TalkerWalker の両方の Protocol を実装しており、どちらの Protocol としても利用できます。

複合プロトコル

複数の Protocol を満たす制約を作ることができる、複合プロトコルという機能もあります。 Protocol そのものと、満たしたい Protocol を継承して、1 つの Protocol として定義できます。

from typing import Protocol
class Talker(Protocol):
    name: str
    def talk(self) -> None:
        pass

class Walker(Protocol):
    name: str
    def walk(self) -> None:
        pass

class TalkerWalker(Talker, Walker, Protocol):
    pass

@abstractmethod について

Protocol では、@abstractmethod の記述がなくても実装がないものは抽象メソッドとして扱われますが、可読性を高めるために、@abstractmethod を使用することが推奨されます。

class Animal(Protocol):
    name: str
    @abstractmethod
    def talk(self) -> None:
        raise NotImplementedError

@runtime_checkable

@runtime_checkable3を定義に付けることで、Protocol を実行時にチェックできます。

注釈 runtime_checkable() will check only the presence of the required methods or attributes, not their type signatures or types. For example, ssl.SSLObject is a class, therefore it passes an issubclass() check against Callable. However, the ssl.SSLObject.init method exists only to raise a TypeError with a more informative message, therefore making it impossible to call (instantiate) ssl.SSLObject.

ただし、メソッド名の一致しか確認しないとのことで、シグネチャのチェックには使えないようですね、残念.....

Generic protocols

Protocol は、ジェネリックに定義する4ことも可能です。

from typing import Protocol, TypeVar
T = TypeVar('T')
class Container(Protocol[T]):
    def add(self, item: T) -> None:
        pass

ただし、Python 3.12 で導入された、Protocol, Generic[T, S, ...]などは使用できません。

まとめ

最後に Protocol についてのまとめになります。

メリットとしては、

  • 明示的な継承を行わずに、インターフェースを実現できる
  • Duck Typing の機構を維持しながら、mypy 等で静的な型チェックが可能

デメリットとしては、

  • 明示的にどの Protocol を実装しているかがわからないため、コードの理解が難しくなることがある
    • 実装させる対象を人為的に規制できない
  • 静的解析のサポートが主であり、実行時エラーは基本起きないので、実装が間違っていても気づきにくい

といった点が挙げられるので、Linter を利用する前提で、Duck Typing を行いたい場合・柔軟に複数のサブタイプとして扱いたい場合などで使用するのが良いように思いました。

また、利用シーンとして以下が考えられるのではないかと思いました。

  • クラスごとの書き換えが必要な、大きなリファクタを行う際に、ABC から Protocol に置き換え、変更がしやすい状態を作る。
  • Interface の変更が頻繁に起こるケースや多重継承が必要なケースでは、あらかじめ ABC で定義せず、柔軟に対応させやすい Protocol を使う。

Protocol の仕様については、紹介しきれなかったものもあるので、興味のある方は公式ドキュメント5を読んでいただければと思います。

弊社 Belong では一緒に働く仲間を募集しています。

Python を使った課題解決や、Go でのサーバーサイド開発に興味のある方など、
エンジニアリングチーム紹介ページをご覧いただけると幸いです。

Footnotes

  1. https://www.oreilly.co.jp/books/9784814400171/

  2. https://peps.python.org/pep-0544/

  3. https://docs.python.org/ja/3.10/library/typing.html#typing.runtime_checkable

  4. https://typing.python.org/en/latest/spec/protocol.html#generic-protocols

  5. https://typing.python.org/en/latest/spec/protocol.html