質問の歴史。 Python 3.7以前、ジェネリック型を実装するには、List[int]のようなサブスクリプションを処理するために__getitem__をインターセプトする複雑なメタクラスTypingMetaが必要でした。このアプローチは遅く、typingモジュール自体内に循環依存性を生み出し、すべてのジェネリック操作が重いメタクラスのロジックを通過するため、デバッグが困難でした。PEP 560は、これらのパフォーマンスとアーキテクチャの問題を解決するために専用のプロトコルを導入しました。
問題。 ジェネリッククラスは、実インスタンスを作成することなく、静的型チェックとランタイム内省をサポートするために、クラスレベルで型引数(List[int]のintのように)を受け入れる必要があります。課題は、軽量オブジェクトにこれらの引数を保存し、ジェネリックの起源とそのパラメータとの関係を保ちながら、クラスを再帰的にサブスクリプトできるようにし、__init__を呼び出さないようにすることでした。
解決策。 Python 3.7+は、Generic基底クラスに__class_getitem__ダンダーメソッドを実装しており、これはクラスがサブスクリプトされたときに自動的に呼び出されます(例:Container[int])。このメソッドは、元のクラスを__origin__に保存し、型引数を__args__に格納するGenericAliasオブジェクト(CPythonの内部型_GenericAlias)を返します。このメカニズムはインスタンス化を完全に回避し、効率のためにこれらのエイリアスオブジェクトをキャッシュします。
from typing import Generic, TypeVar T = TypeVar('T') class Container(Generic[T]): def __init__(self, value: T) -> None: self.value = value # ランタイムのサブスクリプションは、インスタンスではなくGenericAliasを作成します SpecializedType = Container[int] print(SpecializedType) # <class '__main__.Container[int]'> print(SpecializedType.__origin__) # <class '__main__.Container'> print(SpecializedType.__args__) # (<class 'int'>,) # インスタンス化は別々に行われます instance = SpecializedType(42)
問題の説明。 データ検証ライブラリは、ユーザー提供の型ヒントに基づいて、ネストされたJSON構造をPythonオブジェクトにパースする必要がありました(例:Dict[str, List[User]]またはOptional[Tuple[int, str]])。コアの課題は、ランタイムにおいて、ジェネリックコンテナに含まれる型を判断し、適切なサブオブジェクトを再帰的にインスタンス化する必要がありましたが、すべての可能なジェネリックの組み合わせをハードコーディングしないことでした。
解決策1:型表現の文字列パース。 利点:str(type_hint)と正規表現を使って迅速に実装。 欠点:非常に壊れやすく、前方参照、型のユニオン、またはネストされたジェネリックで失敗し、異なるモジュール内の類似の名前の型を区別できません。
解決策2:ユーザーがすべてのジェネリッククラスをデコレートする必要がある手動メタクラス登録。 利点:型パラメータの保存と取得を完全に制御。 欠点:ライブラリユーザーに重い負担をかけ、既にカスタムメタクラスを使用しているクラスでメタクラスの競合を生じ、標準ライブラリに既に存在する機能を重複させます。
解決策3:__class_getitem__の内省を利用したget_origin()とget_args()。 利点:標準のGenericAliasプロトコルを利用し、任意にネストされた構造を堅牢に処理し、追加のユーザーコードなしで複雑な継承階層に対してMROを尊重します。 欠点:__origin__のような内部属性を理解する必要がありますが、これは技術的に実装の詳細であり、現代のPythonバージョンでは安定しています。
選択された解決策。 解決策3は、PEP 560と現代のPython型システムアーキテクチャに合致しているため、選ばれました。型ヒントに対してget_origin(type_hint)をチェックして基底コンテナ(例:dict)を見つけ、get_args(type_hint)を利用してパラメータ化された型(例:str、User)を抽出することにより、ライブラリは再帰的にバリデーターを構築します。このアプローチは、Generic[T]を継承するユーザー定義ジェネリックとシームレスに動作し、クラス定義の変更を必要としません。
結果。 ライブラリは、複雑なネストされたペイロードを型安全なPythonオブジェクトに正常にデシリアライズします。ユーザーはclass PaginatedResponse(Generic[T]): ...を定義でき、システムは自動的にPaginatedResponse[OrderDetail]に遭遇したときにTを抽出し、正しいジェネリックサブツリーをインスタンス化し、IDEサポートとランタイムバリデーションのために完全な型情報を維持します。
なぜisinstance([1, 2, 3], List[int])はTypeErrorを引き起こし、この制限はジェネリック型エイリアスと具体的なランタイム型との違いをどのように反映していますか?
Pythonのisinstanceは、その第二引数が型、型のタプル、または__instancecheck__メソッドを持つオブジェクトである必要があります。List[int]は、__class_getitem__によって生成されたGenericAliasオブジェクトであり、クラスではありません。Pythonは段階的型付けを使用しているため、ジェネリックパラメータはランタイムで消去されます。リスト[1,2,3]は、List[int]とList[str]としてパラメータ化されていることを記憶していません。GenericAliasに対してisinstanceを試みると、TypeError: isinstance() arg 2 must be a type, tuple of types, or a unionが発生します。互換性を確認するには、手動で構造を検証するか、@runtime_checkableプロトコルを使用する必要があります。これらはメソッドの存在のみをチェックし、ジェネリックパラメータをチェックしません。
__class_getitem__は、クラスが複数の特殊化されたジェネリック親から継承する場合、メソッド解決順序とどのように相互作用しますか?例:class MyMapping(Dict[str, int], Mapping[str, Any])
PythonがMyMappingを作成する際、各基底クラスを処理します。Dict[str, int]とMapping[str, Any]は、それぞれの起源に対する__class_getitem__呼び出しから派生したGenericAliasオブジェクトです。MRO計算はこれらを異なる基底として扱いますが、Genericメカニズムは、型引数情報を保持するために__orig_bases__に元のサブスクリプトされた基底を格納します。これにより、get_type_hints(MyMapping)は、MyMappingがDictブランチからstrとintでパラメータ化されていることを解決しますが、Mappingブランチは構造的一致性を提供します。重要な点は、継承の過程で__class_getitem__は再度呼び出されず、既存のエイリアスが新しいクラスに添付され、特定の抽象ベースクラスのために__mro_entries__が最終的なMROを調整し、ジェネリック起源クラスが正しく表示されるようにするかもしれません。
ジェネリッククラス定義での__parameters__と特殊化されたGenericAliasでの__args__の違いは何ですか、また、TypeVarでジェネリックをサブスクリプトすると__args__にTypeVarオブジェクト自体が含まれるのはなぜですか?それは、その束縛ではなくです。
__parameters__は、クラスヘッダで宣言された正式なTypeVarオブジェクト(例:T)を含むクラス属性のタプルであり、ジェネリックの抽象型スロットを表します。__args__は、__class_getitem__によって生成されたGenericAliasインスタンスに現れ、その引数として置き換えられる具体的な型(例:int)を含みます。Container[T]を作成する際、TがTypeVarである場合(他のジェネリック関数内で一般的)、__args__にはTypeVarインスタンスが含まれます。なぜなら、具体的な束縛は外側のスコープが特定の型を提供するまで遅延されるからです。このメカニズムは、高階ジェネリックパターンをサポートし、Callable[[T], T]のような型が、型の入力と出力の関係を複数のジェネリック抽象化レベルにわたって保持することを可能にし、final resolutionがtyping.get_type_hints()を介して発生する際にTypeVarの__bound__属性を使用します。