PythonのProtocolってなに?

mv
会社ブログ Python

PythonのProtocolを知ってますでしょうか?

ProtocolとはPython 3.8 以降で導入された構造的部分型を実現するための機能です。

「特定のメソッドや属性を持っていれば、その型とみなす」というダックタイピングの考え方を、静的型チェック等で利用可能にしたものです。

「もしそれがアヒルのように歩き、アヒルのように鳴くなら、それはアヒルである」

「その型とみなす」とはどういうことでしょうか?

継承との違いもまとめている記事を紹介いたします。

https://zenn.dev/ibaraki/articles/bef0b43522475b

ここからは簡単にはなりますが、Protocolによる恩恵について以下を紹介させていただきます。

  1. 属性やメソッド不足による型エラー
  2. 依存性逆転の原則(DIP)
  3. インターフェース分離の原則(ISP)
  4. 開放閉鎖の原則(OCP)

属性やメソッド不足による型エラー

紹介した記事でもまとめられているため詳細は省略しますが、Protocolによって定義された属性やメソッドがそのオブジェクトに存在しない場合、型チェックツールによりエラーとして検出されるようになります。

Pythonなのでエラーが出ても実行は可能ですが、開発時における型のメリットは非常に大きいですよね。

依存性逆転の原則(DIP)

抽象(インターフェース)に依存することで、上位モジュールが下位モジュールに依存せずに実装できます。

Python
class Speaker(Protocol):
    def output(self, data: str) -> None:
        ...


class MusicPlayer:
    def __init__(self, speaker: Speaker): # 抽象のProtocolに依存
        self.speaker = speaker


# SonySpeaker自体はSpeakerを知らない
class SonySpeaker:
    def output(self, data: str) -> None:
        print("再生します。")
Python

インターフェース分離の原則(ISP)

もしWorkerというクラスに「work」や「eat」がabstractmethodとして定義されていたら、Workerを継承したRobotWorkerも不要なメソッドの実装が強制されてしまいます。

Python
class Worker(ABC):
    @abstractmethod
    def work(self) -> None:
        pass

    @abstractmethod
    def eat(self) -> None:
        pass


# 人間の労働者 - すべてのメソッドが意味を持つ
class HumanWorker(Worker):
    def work(self) -> None:
        print("Working...")

    def eat(self) -> None:
        print("Eating lunch...")


# ロボット労働者 - 不要なメソッドも実装を強制される
class RobotWorker(Worker):
    def work(self) -> None:
        print("Working...")

    def eat(self) -> None:
        # ロボットは食べない
        pass
Python

Protocolは必要な分だけ細かく定義できるため、ロボットに不要なメソッドを強制させません。

Python
class Workable(Protocol):
    def work(self) -> None:
        ...


class Eatable(Protocol):
    def eat(self) -> None:
        ...


# 人間 - 必要な機能をすべて実装
class HumanWorker:
    def work(self) -> None:
        print("Working...")

    def eat(self) -> None:
        print("Eating lunch...")


# ロボット - 必要な機能だけを実装(eatは実装しない)
class RobotWorker:
    def work(self) -> None:
        print("Working ...")

# 必要なメソッドだけを要求
def assign_work(worker: Workable) -> None:
    worker.work()

def provide_lunch(worker: Eatable) -> None:
    worker.eat()
Python

開放閉鎖の原則(OCP)

オープン・クローズドの原則は「拡張に対して開かれ(Open)」、「変更に対して閉じている(Closed)」必要があります。

以下のように新しい支払い方法を後から追加するときでも既存コードを変更する必要がなく、変更に閉ざされたコードを実現できます。

Python
# 抽象:支払い処理のインターフェース
class PaymentMethod(Protocol):
    def process(self, amount: float) -> None:
        ...


# 支払い処理クラス
class PaymentProcessor:
    def __init__(self, payment: PaymentMethod):
        self.payment = payment

    def process_payment(self, amount: float) -> None:
        self.payment.process(amount)


# 拡張:新しい支払い方法を追加
class CreditCardPayment:
    def process(self, amount: float) -> None:
        print("クレカ払い")


class CashPayment:
    def process(self, amount: float) -> None:
        print("現金払い")

# 都度新しい支払い方法を追加
.
.
.
Python

紹介した利点は一例であり、Protocolに限った恩恵ではありません

Protocolを使わずとも継承等でも実現可能ですが、継承関係に縛られないProtocolはより柔軟な設計を可能にします。

よければ「継承不要で型安全」というProtocolならではの快適さを、ぜひ手元のコードで体感してみてください!

お問い合わせ

ご相談・お見積り・ご応募など、お気軽にご連絡ください。