この記事は「KLab Engineer Advent Calendar 2025」の3日目です
Python には「ダックタイピング」と、「継承にもとづいたプログラミング」や「静的な型チェッカー」との仲立ちをしてくれる機能たちが存在しています。それらについて紹介していきます。
抽象基底クラス (ABC, Abstract Base Classe) とは
Python の抽象基底クラス, ABC, Abstract Base Class とは、ダックタイピングと継承に基づいたプログラミングとの仲立ちをしてくれる機能です。
継承してつかうことができる通常のクラスとしての機能を持ちつつ、それとは別に他のクラスとの仮想の親子関係を構築することができます。
from abc import ABC
class Runnable(ABC):
def run(self):
print('run')
@classmethod
def __subclasshook__(cls, C):
# run メソッドを持っているかを確認する
if cls is Runnable:
if any("run" in B.__dict__ for B in C.__mro__):
return True
return NotImplemented
class Child(Runnable):
def run(self):
super().run()
print('child')
class Other0:
def run(self):
print('Other0')
class Other1:
...
# Other1 を Runnable の仮想の子クラスとして登録
Runnable.register(Other1)
# OK 。子クラスなので。
assert issubclass(Child, Runnable)
# OK 。 run メソッドを持っているので(Runnable.__subclasshook__(Other0) が True を返すので)
assert issubclass(Other0, Runnable)
# OK 。 Runnable.register で子クラスであるとされたので
assert issubclass(Other1, Runnable)
抽象基底クラスのよいところ
仮想の子クラスとして登録する機能は、 Python のクラスに既存のコード・クラスの継承関係に手を入れずに親子関係を足すことができる柔軟さを提供してくれます。
Python 公式ドキュメントの numbers モジュールの説明では、自作クラス MyFoo を既存のクラス Complex と Real の間に差し込む例があげられています。
抽象基底クラスと親クラスのメソッドの困るところ
仮想の子クラスでは仮想の親クラスのメソッドを探しに行くことはありません。
Other1().run() # AttributeError
super で探しに行くことにも失敗します。
class Spam:
def spam(self):
super().run() # 見つからない
Runnable.register(Spam)
Spam().spam() # エラー
子クラスであると認識させた後、きちんと動作をさせるのはプログラマーの責務となります。よくもわるくも、ゆるい感じの機構です。
抽象基底クラスと静的型チェッカーの困るところ
mypy をはじめとした静的型チェッカーが仮想の親子関係を認識できないという問題があります。
a: Runnable = Child() # OK
b: Runnable = Other0() # mypy がエラー、 Incompatible types in assignment
c: Runnable = Other1() # mypy がエラー、 Incompatible types in assignment
プロトコル typing.Protocol とは
ABC によるダックタイピング・仮想の親子関係には静的型チェッカーが対応できていません。この問題に対する回答の一つに typing.Protocol があります。ダックタイピングに基づいて作られたクラスたちに対し mypy をはじめとした静的型チェッカーがチェックを行うことができるようになります。
たとえば、引数なし・戻り値 None の run メソッドを持っているというプロトコル、 RunnableProtocol は次のようにして作成できます。これでもって mypy でチェックを行うと次のような結果となります。
from typing import Protocol
class RunnableProtocol(Protocol):
def run(self) -> None:
...
d: RunnableProtocol = Child() # OK
e: RunnableProtocol = Other0() # OK
f: RunnableProtocol = Other1() # mypy がエラー、 run メソッドを持っていない
プロトコルは静的型チェッカーが用いるためのものです。
プロトコルを isinstance, issubclass に用いることはできません。
また、クラスのように実行してもインスタンスを作成することができるものではありません。
issubclass(Other1, RunnableProtocol)) # TypeError, プロトコルはチェックに用いることはできない
RunnableProtocol() # TypeError, インスタンスの作成は不可
プロトコルでも isinstance, issubclass チェックがしたい?
もし、プロトコルでも直接のチェックができるようにしたいという場合には、 runtime_checkable デコレータを用います。
from typing import runtime_checkable
@runtime_checkable
class RunnableProtocolCheckable(Protocol):
def run(self) -> None:
...
assert issubclass(Other1, RunnableProtocolCheckable) # OK
公式ドキュメントの注釈にもあるように、実行時間に影響を及ぼす問題があります。あまり筋のよい手法ではないようです。実行時ではなく実装時にチェックを行いましょう、と。
まとめ
「ダックタイピング」と、「継承にもとづいたプログラミング」や「静的な型チェッカー」との仲立ちをしてくれる機能、抽象基底クラスとプロトコルについて紹介しました。
また、プロトコルを用いることでダックタイピングと静的型チェッカーとの仲立ちができることを紹介しました。