PythonのProtocolによるstructural subtypingでインタフェースを記述する

python

interfaceが文法に存在しないPythonで関数が呼べることを保証する方法の一つに 組み込み関数hasattr()によるチェックがあるが、 都度処理を挟む必要があるのと、実行してみないと分からない問題があった。

class ImplClass():
  def foo(self):
    print("ok")

class NoImplClass():
  pass

def call(d):
  assert hasattr(d, 'foo')
  d.foo()

if __name__ == "__main__":
  call(ImplClass())   # => ok
  call(NoImplClass()) # => AssertionError

Python 3.5で実装されたType Hintsで共通の基底クラスを型として取れば実行前にmypyによる静的解析で検知できるが、サブクラスでの実装は強制できない。

PythonのType Hintsとmypy - sambaiz-net

class BaseClass:
  def foo(self):
    print("please implement this")

class ImplClass(BaseClass):
  def foo(self):
    print("ok")

class NoImplClass(BaseClass):
  pass

def call(d: BaseClass):
  d.foo()

if __name__ == "__main__":
  call(BaseClass())   # => please implement this
  call(ImplClass())   # => ok
  call(NoImplClass()) # => please implement this

それを解決するのがPEP3119のAbstract Base Classesで、 次のようにabc.ABCのサブクラスで @abstractmethod を付けた関数を定義すると、 実装していないサブクラスのインスタンスを作るところでmypyがエラーを検知する。

from abc import ABC, abstractmethod

class AbstractClass(ABC):
  @abstractmethod
  def foo(self):
    pass

class ImplClass(AbstractClass):
  def foo(self):
    print("ok")

class NoImplClass(AbstractClass):
  pass

def call(d: AbstractClass):
  d.foo()

if __name__ == '__main__':
  # AbstractClass() # -> TypeError: Can't instantiate abstract class AbstractClass with abstract methods foo
  call(ImplClass()) # -> ok 
  # NoImplClass()   # -> TypeError: Can't instantiate abstract class ImplClass with abstract methods foo

ただ、明示的にサブクラスにする必要がありduck typingの柔軟性はない。

そこで登場するのがPython 3.8で実装されたPEP544Protocolで、 次のようにProtocolのサブクラスをTyped Hintsに記述すると、その変数や関数が含まれる任意のクラスをサブタイプとみなせるstructural subtyping (static duck typing)が実現される

from typing import Protocol

class ProtocolClass(Protocol):
  def foo(self):
    pass

class ImplClass:
  def foo(self):
    print("ok")

class NoImplClass:
  pass

def call(d: ProtocolClass):
  d.foo()

if __name__ == '__main__':
  # ProtocolClass()     # -> Cannot instantiate protocol class "ProtocolClass"
  call(ImplClass())     # -> ok 
  # call(NoImplClass()) # -> Argument 1 to "call" has incompatible type "NoImplClass"; expected "ProtocolClass"