Pythonは、抽象基本クラスを正式化するために、バージョン2.6でabcモジュールを導入し、従来のダックタイピングを超えて構造的なサブタイプを可能にしました。コアメカニズムは__subclasshook__クラスメソッドであり、これはissubclass()がABCのMRO内で候補を見つけられない場合にabcの仕組みが呼び出します。このメソッドは候補クラスを受け取り、True、False、またはNotImplementedを返し、継承なしでの仮想登録を許可します。
問題は、__subclasshook__が候補が特定のメソッドや属性を実装していることを検証する必要があるために生じます。ガード条件がない場合、フックが内部でissubclass()や同様のチェックを呼び出して同じABCに戻ってしまうと、無限再帰が引き起こされます。必須のガードは、メソッドの開始時にif cls is MyABCを確認することを要求し、それによりフックがその定義を持つ特定のABCだけを検証することを保証し、そのABCのサブクラスを検証しないようにします。
from abc import ABC, abstractmethod class Drawable(ABC): @abstractmethod def draw(self): pass @classmethod def __subclasshook__(cls, C): # 再帰に対するガード:Drawableを直接扱う if cls is not Drawable: return NotImplemented # 構造的チェック:Drawableのように歩き、話すか? if hasattr(C, "draw") and callable(getattr(C, "draw")): return True return NotImplemented class Circle: def draw(self): print("Drawing circle") # 継承なしでの仮想サブクラスの検証 assert issubclass(Circle, Drawable)
私たちのチームは、複数のデータベースバックエンドをサポートする必要がある統合分析プラットフォームを構築していました。connect()、execute()、およびclose()などのメソッドを持つDatabaseDriver ABCを定義しました。しかし、psycopg2やpymongoのような既存のサードパーティのデータベースライブラリをフォークせずにサポートしたいと考えていました。
最初に考えたソリューションは、厳密なアダプタパターンの継承でした。私たちは、サードパーティの接続をカプセル化するためにPsycopg2Adapter(DatabaseDriver)のようなラッパークラスを作成しました。これにより、完璧なタイプ安全性と静的分析サポートが提供されました。しかし、これは各メソッドの委任に対してかなりのメンテナンスオーバーヘッドを生み出し、ランタイムでの二重間接呼び出しのオーバーヘッドも導入しました。
第二のアプローチは、ランタイムでの属性検査を持つ純粋なダックタイピングでした。私たちは、connectおよびexecuteメソッドを持つオブジェクトは有効なドライバーであると単純に仮定しました。これは最大の柔軟性とボイラープレートなしを提供しましたが、メソッドシグネチャが互換性がないときに静かに失敗しました。さらに、mypyのような静的型チェッカーはこれらの契約を検証できず、プロダクション環境でのエラー検出の遅延を招きました。
私たちは第三のソリューションを選びました:DatabaseDriver ABCにおいて__subclasshook__を実装して仮想サブクラスを登録しました。これにより、ラッパークラスが不要になり、厳密なisinstance検証を維持し、サードパーティのクラスが変更なしでタイプチェックに合格できるようになりました。ガード条件により、DatabaseDriverのサブクラスを自分自身に対してチェックすることが無限ループを引き起こさないように保証されました。
その結果、アダプタのボイラープレートコードが40%削減され、IDEの自動補完サポートもシームレスに提供されました。システムは、私たちのABCに関して何も知らないライブラリからの生のデータベース接続を受け入れることができるようになり、同時に厳格なランタイム検証と構造的型保証を維持できました。
なぜ__subclasshook__は、構造的チェックを行う前にif cls is MyABCをチェックしなければならず、このガードを省略するとどうなるのか?
このガードがなければ、issubclass(SubClass, MyABC)を呼び出すとMyABC.__subclasshook__(SubClass)がトリガーされます。もしフックが内部でissubclass(SubClass, MyABC)をチェックして継承を確認しようとすると、即座に無限再帰を引き起こします。Pythonのabcの仕組みは、フックを定義する正確なクラスのみに呼び出しますが、構造的チェックは同じクエリに戻ることがよくあります。ガードがなければ、フックがその定義を持つ特定のABCのみを検証することを保証することができず、スタックはすぐにオーバーフローします。
register()による仮想サブクラス化は、性能と可変性の点で__subclasshook__とどのように異なるのか?
register()は、クラスを内部キャッシュ(_abc_cache)に即座に追加し、その後のチェックをO(1)のセットルックアップで行えるようにします。それに対して、__subclasshook__は毎回のissubclass呼び出しで任意のPythonコードを実行し、キャッシュされていない限り計算オーバーヘッドを生み出します。さらに、register()はプロセスのライフタイムにわたって永続的であり、listのような組み込みタイプにも適用されます。一方、__subclasshook__はランタイムの機能に基づいた動的で条件付きのロジックを可能にしますが、ユーザー定義のABCにのみ機能します。
__subclasshook__とカスタムメタクラスにおける__instancecheck__メソッドの相互作用はどうなりますか?
isinstance(obj, MyABC)が呼び出されると、Pythonは最初にインスタンスのメタクラスの__instancecheck__を参照します。利用できない場合や結論が出ない場合、issubclass(type(obj), MyABC)にフォールバックし、これが__subclasshook__をトリガーします。候補者は、__subclasshook__がクラスチェックにのみ参加し、直接のインスタンスチェックには関与しないことを見落とすことが多いです。また、NotImplementedを返すことで、チェックがMROを通じて続行できることを見落とし、複雑な階層間での協調的な多重ディスパッチを可能にします。