PEP-544 (Protocols: Structural subtyping (static duck typing)) を読んだよメモ


ある議論の流れで PEP 544 -- Protocols: Structural subtyping (static duck typing) を読むことになったので、自分の理解をメモに残しておく。

概要

  • PEP 484 (Type Hints) では、あるインターフェースの実装を表現するには継承が必要とされていた(いわゆる nominal subtyping)
  • PEP 544 では、プロトコルクラスを提起することで継承を伴わない型チェックにも対応する (いわゆる structural subtyping)
  • ダックタイプの文化を持つ Python では structural subtyping の導入は自然である

アプローチ

プロトコルの定義

typing.Protocol を継承したクラスをプロトコルクラス(もしくは単にプロトコルとも)と呼ぶ。
プロトコルクラスにはプロトコルメンバーとして、どういう型のどういうメンバー(変数、メソッド)を持つのかを定義できる。

次の例では「引数無しで、返り値を持たない close() というメソッドをひとつ持つ」 SupportsClose プロトコルを定義している。

from typing import Protocol

class SupportsClose(Protocol):
    def close(self) -> None:
        pass

たとえば、次のようなクラスは SupportsClose プロトコルを満たしていると言える。

class Resource:
    ...
    def close(self) -> None:
        self.file.close()
        self.lock.release()

そのため、次のようなコードは意図通り型チェックされる。

def close_all(things: Iterable[SupportsClose]) -> None:
    for t in things:
        t.close()

f = open('foo.txt')
r = Resource()
close_all([f, r])  # OK!
close_all([1])     # Error: 'int' has no 'close' method

プロトコルメンバー(変数)の定義

変数アノテーションを使って、変数をプロトコルメンバーとして定義できる。
なお、メソッドの中で変数を初期化する場合はプロトコルメンバーにはならないため、以下のコードで言う self.temp は無視される (プロトコルの型チェックには影響しない)。

from typing import Protocol, List

class Template(Protocol):
    name: str        # This is a protocol member
    value: int = 0   # This one too (with default)

    def method(self) -> None:
        self.temp: List[int] = [] # Error in type checker

ちなみに、クラス変数の場合は typing.ClassVar を使いましょう、とのこと1

プロトコルクラスの継承

ここまではプロトコルクラスを型として扱う (型アノテーションする) ものとして説明してきたが、プロトコルクラスはふつうの Python クラスであるため、継承して派生クラスを作ってもよい。

class PColor(Protocol):
    @abstractmethod
    def draw(self) -> str:
        ...
    def complex_method(self) -> int:
        # some complex code here

class NiceColor(PColor):
    def draw(self) -> str:
        return "deep blue"

継承した場合はメンバーがそのまま派生クラスに継承されるので、もちろんプロトコルを満たすとみなされる 2

単に型チェックをするだけであれば継承する必要はないので、この使い方をする場合は実装を共有できるのが利点と考えるのが良さそうです。abc.abstractmethod と組み合わせて使う例なども紹介されていて、ABC (Abstract Base Classes)ライクな紹介をされています3

コールバックプロトコル

可変引数やオーバーロード、Generic などの複雑な callable インターフェースを表現するのに、__call__() メソッドを持ったプロトコルクラスを使うといいよ、とありました。これは便利そう。

from typing import Optional, List, Protocol

class Combiner(Protocol):
    def __call__(self, *vals: bytes,
                 maxlen: Optional[int] = None) -> List[bytes]: ...

感想

  • 最初に PEP-554 を流し読みしたときは、タイトルの影響もあってプロトコルクラス = structural subtyping として理解していたので、ちゃんと読んでみたら実装継承もできるよ! って書いてあってびっくりした。完全に記憶から抜け落ちていた。
  • プロトコルクラスは nominal subtyping 的に(explicit に)継承して使うこともできるし、structural subtyping 的に(implicit に)継承せずにも使えるので非常に分かりづらい。
  • なにはともあれ Python 3.8 からは自分でプロトコルを定義できるようになったのはめでたい。

  1. 手元で実験してみたけどうまく行かないが…?  

  2. 敢えてプロトコルを満たさないように派生クラスでオーバーライドすることもできますが、イレギュラーな使い方なので説明は端折ります。 

  3. というか、型ヒント以外の振る舞いは(= 実行時は)ほぼ ABC と同等とみなして良さそう。