PythonのABCとProtocolは、実行時にも isinstance() で判定できます。ただし、同じ True が返っても、確認している内容は同じではありません。
- ABCは、基本的に継承関係を確認する
- ABCの仮想サブクラスは、登録または独自の判定条件を使う
- runtime-checkable Protocolは、定義されたメンバーの存在を確認する
ABCとProtocolの基本的な違いについては、PythonのABCとProtocolはどう使い分ける?で整理しています。この記事では、register()、__subclasshook__()、@runtime_checkable を使った実行時検証に絞ります。
コード例はPython 3.12以降を対象とします。紹介するAPI自体はPython 3.8以降でも利用できます。
実行時検証の違いを先に整理する
| 方法 | 実行時に主に確認するもの | 注意点 |
|---|---|---|
| ABCの明示的なサブクラス | 継承関係と抽象メソッドの実装 | 実装側がABCへ依存する |
ABC + register()
|
開発者が登録したクラス | 登録時に構造を自動検証しない |
ABC + __subclasshook__()
|
開発者が実装した判定条件 | シグネチャ検証まで行うなら自前実装が必要 |
Protocol + @runtime_checkable
|
Protocolメンバーの存在 | シグネチャや型は検査しない |
ABCで継承なしのクラスを仮想サブクラスとして扱う
ABCは通常、明示的な継承を使います。ただし、継承していないクラスを仮想サブクラスとして扱う方法もあります。
register() で仮想サブクラスとして登録する
from abc import ABC
class Serializer(ABC):
pass
class ThirdPartySerializer:
def serialize(self, value: object) -> bytes:
return str(value).encode()
Serializer.register(ThirdPartySerializer)
assert isinstance(ThirdPartySerializer(), Serializer)
register() は、指定したクラスを仮想サブクラスとして登録します。ThirdPartySerializer が serialize() を実装しているかどうかは検証しません。
__subclasshook__() で判定条件を実装する
from abc import ABC, abstractmethod
from typing import Any
class Serializer(ABC):
@abstractmethod
def serialize(self, value: object) -> bytes:
raise NotImplementedError
@classmethod
def __subclasshook__(cls, subclass: type[Any]):
if cls is Serializer:
has_serialize = any(
"serialize" in base.__dict__ for base in subclass.__mro__
)
if has_serialize:
return True
return NotImplemented
class ThirdPartySerializer:
def serialize(self, value: object) -> bytes:
return str(value).encode()
assert isinstance(ThirdPartySerializer(), Serializer)
__subclasshook__() は判定ロジックを自分で実装できるため柔軟です。ただし、この例では serialize という名前の存在しか確認しておらず、引数や戻り値の型までは検査していません。
register() や __subclasshook__() で認識された仮想サブクラスは、ABCを実際には継承していません。
ABCの共通実装はMROへ入らず、@abstractmethod の実装強制も受けません。 isinstance() が True になることと、基底クラスの機能を継承することは別です。
runtime-checkable Protocolを isinstance() で判定する
通常のProtocolは、isinstance() の第2引数に指定できません。 @runtime_checkable を付けると実行時検証を利用できます。
from typing import Protocol, runtime_checkable
@runtime_checkable
class Serializer(Protocol):
def serialize(self, value: object) -> bytes: ...
class JsonSerializer:
def serialize(self, value: object) -> bytes:
return b"{}"
assert isinstance(JsonSerializer(), Serializer)
ただし、@runtime_checkable は静的型チェックと同じ検証を実行時に行う機能ではありません。属性が存在するかを確認しますが、メソッドの引数や戻り値の型は検査しません。
class IncompatibleSerializer:
def serialize(self) -> str:
return "not bytes"
assert isinstance(IncompatibleSerializer(), Serializer)
# Trueになり得るが、静的型チェックではSerializerと互換性がない
Python公式ドキュメントでは、runtime-checkable Protocolに対する isinstance() は通常の isinstance() より遅くなる場合があるため、性能を重視するコードでは hasattr() なども検討するよう説明されています。
実行時検証をどう使い分けるか
プラグインをロードする場面で「このフレームワークの正式な実装か」を確認したいなら、ABCの明示的な継承が分かりやすい場合があります。外部オブジェクトが必要な操作を持つかだけを確認したいなら、runtime-checkable Protocolや個別の hasattr() が候補になります。
実行時検証が必要だからといって、すべての境界へ isinstance() を追加する必要はありません。通常のアプリケーションコードでは、静的型チェッカーで契約を確認し、実際の呼び出しで失敗した例外を適切な境界で処理する方法もあります。
入力データの値や形式まで保証したい場合は、どの方法でも十分ではありません。別途バリデーションが必要です。