質問の歴史
__mro_entries__プロトコルは、PEP 560(「タイプモジュールとジェネリック型のコアサポート」)を介してPython 3.7で導入されました。この強化の前は、typing.List[int]などのジェネリックエイリアスをクラス定義の基底クラスとして使用することができませんでした。なぜなら、type.__new__はすべての基底がtypeのインスタンスであることを厳しく要求したからです。この制限により、typingモジュールは維持が難しく、パフォーマンスの問題を引き起こす脆弱なメタクラスハックに依存せざるを得ませんでした。このプロトコルは、基底の構文的表現を継承グラフへの意味的寄与から切り離すように設計されており、ジェネリックおよびファクトリパターンのきれいなサポートが可能になります。
問題
CPythonがクラス定義を処理する際には、C3線形化アルゴリズムを使用してメソッド解決順序(MRO)を計算し、一貫性と予測可能なメソッドルックアップ階層を確保する必要があります。基底オブジェクトがクラスでない場合(例えば、パラメータ化されたジェネリックや設定オブジェクトなど)には、インタプリタは新しいクラスを継承ツリー内の適切な位置に配置するための必要な型情報を欠いています。このようなオブジェクトを単純に無視すると、isinstanceチェックやsuper()チェーンが壊れてしまい、拒否すれば強力なメタプログラミングパターンを妨害します。核心的な課題は、これらの非クラスオブジェクトがクラス構築フェーズ中に論理的に表す具体的なクラスを宣言できるようにすることでした。
解決策
Pythonは現在、クラス作成時に基底タプル内の各アイテムに対して__mro_entries__(self, bases)メソッドを検査します。このメソッドが存在する場合、元の基底タプルで呼び出され、MRO計算に使用する実際のクラスのタプルを返さなければなりません。返されたクラスは、明示的に基底としてリストされたかのように扱われます。このメカニズムにより、インスタンスは定義時に具体的なクラスに解決する透過的なプレースホルダーとして機能できます。
class ConfigurableMixin: def __init__(self, feature): self.feature = feature def __mro_entries__(self, bases): # 設定に基づいて基底クラスを動的に注入 if self.feature == "logging": return (LoggingSupport,) return (BaseFeature,) class LoggingSupport: def log(self, msg): print(msg) class BaseFeature: pass # インスタンスはMRO内でLoggingSupportに置き換えられます class Service(ConfigurableMixin("logging")): pass print(LoggingSupport in Service.__mro__) # True
大規模な非同期ウェブフレームワークでは、開発者が特定のデータベースURL(例:DatabaseMixin("postgresql://"))でインスタンス化されると、自動的にConnectionPoolとAsyncSessionの両方をユーザーのサービスクラスに基底クラスとして注入するDatabaseMixinファクトリーを作成する必要がありました。問題は、DatabaseMixin(...)がクラスではなくプレーンオブジェクトインスタンスを返すため、開発者が明示的にclass UserService(ConnectionPool, AsyncSession)と記述したかのようにMROに参加する必要があるということでした。
解決策 1: カスタムメタクラス
1つのアプローチは、__new__でbasesタプルをスキャンし、DatabaseMixinインスタンスを特定し、それをターゲットクラスで置き換えるメタクラスを作成することでした。これにより、正確な制御が可能でしたが、「メタクラスの競合」という問題を導入しました。このメタクラスを使用するサービスは、特定のORM基底クラスなど、独自のメタクラスを定義している他のクラスから継承することができませんでした。さらに、クラス定義の構文は複雑な変換を隠し、スタックトレースはメタクラスの内部に指摘されるため、デバッグが困難になりました。
解決策 2: クラスの生成後装飾
もう1つのオプションは、クラスが作成された後に適用されるクラスデコレーターを使用することでした。デコレーターは、ConnectionPoolとAsyncSessionのメソッドを新しいクラスに手動でコピーしたり、type.__setattr__を使用してそれらを注入したりします。これにより、メタクラスのウイルス感染を回避しましたが、根本的にPythonの継承モデルを壊しました:isinstance(UserService(), ConnectionPool)はFalseを返し、コピーされたメソッド内のsuper()呼び出しは正しく解決されませんでした。なぜなら、MROには親クラスが実際には含まれていなかったからです。これにより、フレームワークユーティリティがサービスをデータベース対応として認識できない微妙なバグが発生しました。
解決策 3: __mro_entries__プロトコル
チームは、DatabaseMixinによって返されたオブジェクトに__mro_entries__を実装することを選びました。このメソッドは、解析されたURLに基づいて(ConnectionPool, AsyncSession)を返しました。この解決策は、CPythonのネイティブなクラス作成メカニズムとシームレスに統合されました。MROは正しく計算され、isinstanceチェックは自然に機能し、メタクラスの競合はありませんでした。ファクトリーインスタンスは宣言的なプレースホルダーとして機能し、クラス構築中に適切な継承構造に溶け込み、super()の文脈と多重継承との互換性を保ちました。
その結果、開発者はclass OrderService(DatabaseMixin(postgres_url)):と記述するだけで、コネクションプーリングとセッション管理の機能を正しいメソッド解決、完全なIDEサポート、ランタイムオーバーヘッドまたは継承の競合なしで自動的に受け取ることができるクリーンで直感的なAPIを持ちました。
__mro_entries__が基底をクラスに展開するとき、C3線形化は潜在的な重複をどのように扱いますか?
__mro_entries__が基底に既に他の場所に存在するクラスを返す場合(例えば、1つのファクトリーが(BaseA,)に展開し、他の明示的基底がDerived(BaseA)である場合)、PythonのC3アルゴリズムは、展開されたタプルを実効基底リストとして扱います。このアルゴリズムは、これらのリストをマージし、ローカルの優先順序を保持し、単調性を確保します。C3は共通の先祖を扱うように設計されているため、BaseAは最終的なMROには1回だけ現れ、その位置はそれに依存するすべてのクラスの後ですが、objectの前になります。候補者はしばしば、これが競合や重複のエントリを生み出すと誤解しますが、線形化プロセスは「子供は親の前に」という制約を保持しながら自然に重複を排除します。
__mro_entries__が作成中のクラスにアクセスできないのはなぜで、アクセスしようとした場合にどのような特定のエラーが発生しますか?
クラス作成中、type.__new__は基底オブジェクトに対して__mro_entries__を呼び出すが、クラスオブジェクト自体がまだインスタンス化されていません。名前空間辞書は存在しますが、クラスオブジェクトはまだアイデンティティを持っていません。実装が予想されるクラスの属性にアクセスしようとすると(例えば、外部スコープからクラス名を参照する場合や、新しいクラスにバインドされるかのようにbasesを検査しようとする場合)、NameErrorまたはAttributeErrorが発生します。候補者はしばしば、クラスの最終状態や__dict__を検査して動的決定を行うことができると仮定しますが、メソッドは元の基底タプルを引数として受け取り、戻り値を決定するために自らの内部状態に依存しなければなりません。
__mro_entries__を介してABCの仮想サブクラスとしてオブジェクトを登録することは、ABCをMROに表示させますか?
いいえ。仮想サブクラス登録は、isinstance()およびissubclass()チェックのために内部キャッシュをABCに補充するランタイムメカニズムです。これはサブクラスの__mro__属性を変更しません。MyClass(MyObject())が定義され、MyObject()が__mro_entries__を介して(ConcreteBase,)を返すとき、ConcreteBaseのみがMyClass.__mro__に現れます。もしConcreteBaseがMyABCの仮想サブクラスとして登録されていれば、isinstance(MyClass(), MyABC)はTrueを返しますが、MyABCはMyClass.__mro__には含まれません。候補者はしばしば、仮想サブクラス化を真の継承と混同し、super()呼び出しやMRO検査がABC関係を反映しない理由、またはABCで定義されたメソッドが継承を介して利用できない理由について混乱が生じます。