ある議論の流れで 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 からは自分でプロトコルを定義できるようになったのはめでたい。