引数なしのsuper()は、Pythonクラス本文内で定義された任意のメソッドのために暗黙的に作成されるコンパイラ生成のクロージャセル__class__に依存しています。コンパイラがクラス定義を処理すると、そのメソッドのクロージャにクラスオブジェクトを指すセル変数__class__が作成されます。引数なしでsuper()が呼び出されると、Cの実装は呼び出しフレームを調べ、この__class__セルを見つけ、それを最初の引数(型)として使用します。次に、メソッドの最初の位置引数(通常はself)をインスタンスとして使用します。このメカニズムは、定義時にクラス参照を束縛し、呼び出し時ではなく、クラス名をハードコーディングする必要を排除し、継承チェーン内の各メソッドが自分の特定の位置に関して参照することを保証します(MRO:メソッド解決順序)。
class Base: def method(self): return "Base" class Middle(Base): def method(self): # __class__はここでMiddleに束縛される return f"Middle -> {super().method()}" class Derived(Middle): def method(self): # __class__はここでDerivedに束縛される return f"Derived -> {super().method()}"
私たちは深い価格モデルの階層を持つ定量取引ライブラリを管理していました。BaseModelはcalculate_risk()メソッドを提供し、EquityModelは株特有のロジックを追加するためにそれをオーバーライドし、AmericanOptionModelはさらに専門化しました。EquityModelをVanillaEquityModelに名前変更するための大規模なリファクタリング中に、コピー&ペーストされたミックスインクラスで古いsuper(EquityModel, self)呼び出しが数十件見つかりました。これらの古い参照はTypeErrorや、間違った親メソッドが呼び出される論理エラーを引き起こし、プロダクションのリスク計算を破壊しました。
解決策1: グローバルな検索と置換のリファクタリング。 すべてのハードコーディングされたクラス名をsuper()呼び出しで見つけて置き換えるために自動化ツールを使用することを検討しました。 利点: アーキテクチャの変更は不要で、レガシーPython 2の構文で動作します。 欠点: 脆弱で不完全であり、動的に生成されたクラス、文字列ベースの動的メソッド割り当て、サードパーティの拡張の参照を見逃します。また、クラス名が各メソッドに繰り返されるため、DRY原則に違反します。
解決策2: 引数なしのsuper()の普遍的な採用。 私たちはコードベース全体を引数なしのsuper()を使用するように移行しました。 利点: これにより、クラス名の変更が完全に安全になり、リファクタリング中の人為的エラーの主要な原因を排除し、冗長なノイズを取り除くことで可読性が大幅に向上します。複雑な協調多重継承パターンを正しく処理します。 欠点: Python 3.6+が必要(私たちはそれを持っていました)、そして暗黙のクロージャメカニズムに不慣れな開発者は初めて混乱しました。また、定義後にクラスに動的に追加された関数では使用できません。
解決策3: メタクラスによるクラス参照の注入。 私たちはメタクラスを使用してすべてのメソッドに_defining_class属性を注入することを短期間で検討しました。 利点: これにより、メカニズムが明示的で検査可能になります。 欠点: これにより、かなりの複雑さとオーバーヘッドが追加され、標準のCPython最適化と対立し、言語コンパイラによってすでに提供されている機能を再発明します。
私たちは解決策2を選びました。移行は1スプリントで完了しました。その結果、クラス名変更に関するその後のリファクタリングタスクにかかる時間が40%削減され、CIパイプライン内の古いsuper()参照に関連する全てのバグが排除されました。
引数なしのsuper()は、どのように物理的に__class__セルを特定するのか?
CPythonにおけるsuper()の実装(Objects/typeobject.c内)は、PyEval_GetLocals()を使用して呼び出しフレームのローカル変数とクロージャを調査します。特に、__class__という名前の自由変数(セル)を探します。このセルは、クラス本文内でレキシカルに定義された関数によってのみコンパイラによって作成されます(CO_OPTIMIZEDフラグとクラススコープによって示される)。もしセルが見つかった場合、super()はクラスオブジェクトを抽出します。見つからない場合、RuntimeError: super(): __class__セルが見つかりませんを発生させます。引数なしの形式は、基本的にコンパイラによってsuper(__class__, self)に変換され、ここで__class__はクロージャ変数です。
クラスが作成された後にクラス属性に割り当てられた関数内で引数なしのsuper()を使用しようとするとどうなりますか?
もし関数をクラス本文の外側に定義してからそれをメソッド(例:MyClass.method = some_function)として割り当てると、その関数内でsuper()を呼び出すとRuntimeErrorが発生します。これは、コンパイラがクラススイートの一部としてコンパイルされたコードオブジェクトに対してのみ__class__セルを作成するためです。セルがなければ、super()は階層内の「現在の」クラスを特定する方法がなく、関数の定義スコープと後でそれに添付されたクラスを区別できません。
なぜ引数なしのsuper()は、サブクラスメソッドがsuper()を呼び出し、親メソッドもsuper()を呼び出したときに無限再帰を引き起こさないのか?
これは、__class__がメソッドが定義されたクラスを参照するからです。インスタンスのランタイムクラス(type(self))ではありません。Derived.method()がsuper()を呼び出すと、__class__はDerivedであり、次のクラスDerived.__mro__(例:Middle)にデリゲートします。Middle.method()に達し、それがsuper()を呼び出すと、固有の__class__セルがMiddleを含んでいるため、次のクラス(例:Base)を見上げます。階層の各レベルは、自分の定義時のクラス参照を使用し、MROを正確に1回だけ線形に上昇させ、サブクラスに戻ることはありません。