PythonProgrammingシニアPython開発者

クラスの継承リストに現れる際、**Python**の非クラスオブジェクトが参加クラスを指定するための動的置換メカニズムは何ですか?それによってメソッド解決順序(MRO)の計算に影響を与えますか?

Hintsage AIアシスタントで面接を突破

この質問への回答

質問の歴史

Python 3.7のPEP 560の採用により、型システムはList[int]Generic[T]のようなジェネリック型を基底クラスとして使用する方法を必要としました。この改善前は、パラメータ化されたジェネリックから継承しようとするとTypeErrorが発生し、これらのオブジェクトは実際のクラスではなかったため、開発者はライブラリ設計を複雑にするメタクラスの回避策に頼る必要がありました。

問題

インタプリタがクラス定義を処理する際、C3線形化アルゴリズムを使用してメソッド解決順序(MRO)を計算しなければなりません。このアルゴリズムは、すべての基底がクラスであることを要求します。基底オブジェクトがクラスでなく、ジェネリックエイリアスである場合、MRO構築中にこのエイリアスの代わりとなる実際のクラスを決定するためのプロトコルが必要です。

解決策

Python__mro_entries__プロトコルを導入しました。クラス作成時にこのメソッドを持つ基底に遭遇した場合、base.__mro_entries__(original_bases)を呼び出し、クラスのタプルを返すことを期待します。これらのクラスは、MRO計算で元の基底の代わりとなります。例えば、typing.Genericはこれを実装して(Generic,)を返し、パラメータ化されたロジックが分離されたままで機能するようにします。

from typing import Generic, TypeVar T = TypeVar('T') # Generic[T]はクラスではないが、__mro_entries__によってクラスとして機能できる class Container(Generic[T]): pass # Container.__mro__にはGenericが含まれ、Generic[T]は含まれない print(Container.__mro__) # (<class 'Container'>, <class 'typing.Generic'>, <class 'object'>)

実生活からの状況

あるフレームワークチームは、ユーザーがModel[UserType]のようなパラメータ化されたジェネリック基底を使用してデータモデルを定義できるようにする必要がありました。彼らの最初のアプローチは、クラス作成を intercept し、型パラメータを抽出するためにカスタムメタクラスを使用していましたが、これによりユーザーはフレームワークとDjangoSQLAlchemyモデルを組み合わせる際にメタクラスの競合を手動で解決する必要がありました。

彼らは、定義後にクラスを書き換えるクラスデコレーターを使用することを検討しましたが、このアプローチは、型チェッカーがソースコードを分析した後に変換が行われるため、静的型チェックやIDEオートコンプリートを破壊しました。別の選択肢として__init_subclass__がありましたが、基底自体がクラスでない場合には対処できませんでした。

チームは、ジェネリックファクトリオブジェクトに__mro_entries__を実装しました。ユーザーがclass UserModel(Model[UserType])と記述すると、Model[UserType]インスタンスはその__mro_entries__メソッドから(Model,)を返しました。これにより、クラスはModelから正しく継承できるようになり、ファクトリはランタイム検証のために特定の型パラメータを保存しました。この解決策はメタクラスの競合を排除し、完全なIDEサポートを維持し、C3線形化アルゴリズムを満たすクリーンな継承階層を保持しました。

候補者が見逃しがちなこと

__mro_entries__はランタイム型チェックやisinstanceの動作に影響を与えますか?

候補者はしばしばMRO構築とインスタンスチェックを混同します。__mro_entries__はクラス作成中にのみ機能し、__mro__タプルを構築します。これは、ランタイムのisinstance()issubclass()チェックに影響を与えません。これらの操作は、既存クラスの__class__および__bases__属性に依存しており、クラス定義フェーズ中に発生した動的置換には依存していません。

なぜ__mro_entries__は単一のクラスではなくタプルを返すのですか?

タプルの戻り値は、複雑な多重継承シナリオに対応します。一般的には(Generic,)のような単一要素のタプルを返しますが、このプロトコルは、複数のミックスインから同時に継承を示すためのジェネリックパラメータを許可します。Pythonは、このタプルをMRO計算のために基底リストに直接解凍しますので、(A, B)を返すことは、クラスが元の非クラス基底の代わりにABの両方から継承することを意味します。

__mro_entries__によって返されるクラスに対してPythonはどのような検証を行いますか?

インタプリタは、返されたクラスが有効な継承グラフを形成するかを厳密に検証します。タプルに、矛盾したMROを生成するクラスが含まれている場合—例えば、C3線形化の制約に違反するダイヤモンド継承の競合を持ち込む場合—Pythonはクラス作成中にTypeErrorを発生させます。この検証により、動的置換が言語の基本的な継承の一貫性ルールを回避することができないことが確保されます。