PythonのProtocolによるstructural subtypingでインタフェースを記述する
pythoninterfaceが文法に存在しない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で実装されたPEP544のProtocolで、 次のように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"