見出し画像

Python 3: Deep Dive (Part 4 - OOP): カスタムメタクラス (セクション14-5/15)

  • prepare メソッドを使うと、クラスの辞書(namespace)をカスタマイズしたり、独自の辞書型を返せるため、クラス定義時の属性収集方法を柔軟に制御できる。

  • メタクラスの call は、クラスがインスタンス化される際の流れ(new → init)を総括し、クラス自身を呼び出し可能にする。

  • Pythonはクラス定義時に、まず prepare で辞書を準備 → クラス本体をその辞書内で実行 → new で最終クラスオブジェクトを生成し、これにより高度なメタプログラミングが可能になる。

Pythonでカスタムメタクラスを使ってクラスを作成する際には、裏側で多くのことが起こっています。クラス辞書が実際にどのように形成されるのか、あるいはクラスオブジェクト自体がどのように呼び出し可能(callable)になるのかは見逃しがちです。本記事では、以下の3つの側面に焦点を当て、Pythonが `class MyClass(metaclass=...)` を処理するときに何を行っているか、その詳細を解説します。

  1. preparenew の前にクラス辞書を構築するために呼び出される仕組み。

  2. 返す辞書オブジェクトをカスタマイズする方法(および、その活用法)。

  3. メタクラス(`type` など)内の call がインスタンス生成を統括する仕組み、そしてクラス自体がどのように呼び出し可能になるのか。

これらを理解すれば、`class MyClass(metaclass=...)` と記述した際に、Pythonがどのように preparecall を繋ぎ合わせて動作しているか、よりはっきりイメージできるはずです。


__prepare__ の役割

次のようなクラス定義があったとします:

class MyClass(metaclass=MyMeta):
    ...

Pythonはこのクラスを最終的なクラスオブジェクトとして構築するために、クラス名や継承元クラス、クラス本体など様々な情報を集めます。そのうちの一つがクラス辞書で、クラス本体に定義された属性(メソッドやクラス変数など)を保持します。意外にも、この辞書は new の中で作られるわけではなく、new が呼ばれる前に、メタクラスの特別なメソッドである prepare を通して生成されます。

デフォルトの挙動

デフォルトでは、`type.prepare は空の `dict` を返します。Pythonは続いて、module, qualname などいくつかのキーをその辞書に挿入し、クラス本体をその名前空間で実行し、最後に `MyMeta.new を呼んで、最終的なクラスオブジェクトを返します。

例を挙げましょう:

class MyMeta(type):
    @staticmethod
    def __prepare__(name, bases, **kwargs):
        print(f'MyMeta.__prepare__ called with name={name}, kwargs={kwargs}')
        return {'a': 100, 'b': 200}
    
    def __new__(mcls, name, bases, cls_dict, **kwargs):
        print('MyMeta.__new__ called...')
        print(f'\tcls_dict = {cls_dict}')
        return super().__new__(mcls, name, bases, cls_dict)

class MyClass(metaclass=MyMeta, kw1=10, kw2=20):
    pass

クラス生成の際、Pythonは以下を行います:

  1. `MyMeta.prepare("MyClass", (), kw1=10, kw2=20)` を呼び出し → `{'a': 100, 'b': 200}` を得る。

  2. そこへ `{'module': 'main', 'qualname': 'MyClass'}` を挿入。

  3. `pass` をその辞書の名前空間で実行(今回は何も追加されない)。

  4. `MyMeta.new(..., cls_dict={'a':100, 'b':200, 'module':..., 'qualname':...}, kw1=10, kw2=20)` を呼び出す。

  5. 返ってきたクラスオブジェクトが最終的な `MyClass` として完成。

つまり、new メソッドが見る辞書には `'a'` や `'b'`、そして modulequalname が入っているわけです。

マッピングを返す必要がある

prepare が返す値は、辞書のように振る舞うマッピング型でなければいけません。文字列や整数を返せば、`"string indices must be integers"` のようなエラーが出ます。また、`collections.UserDict` のように厳密には `dict` のサブクラスでないものを返した場合もエラーになります。実際には、`dict` か、あるいはそれを継承したクラスを返す必要があります。


クラス辞書のカスタマイズ

prepare をオーバーライドすることで、クラスの属性がどのように収集されるかを自在にコントロールできます。例えば:

  • 既定の属性 をクラス辞書にあらかじめ入れておく。

  • (古いPythonバージョンでは)`OrderedDict` を返すことで、定義順を正確に保持する。

  • `dict` を継承したカスタム辞書を使って、クラス本体が属性を定義するたびにロギングや型チェックを行う。

例えば `dict` を継承したクラスを使う例:

class CustomDict(dict):
    def __setitem__(self, key, value):
        print(f'Setting {key} = {value}')
        super().__setitem__(key, value)

class MyMeta(type):
    @staticmethod
    def __prepare__(name, bases):
        return CustomDict()  # 書き込み時にログを出す

    def __new__(cls, name, bases, cls_dict):
        print('Final dictionary in __new__:', cls_dict)
        return super().__new__(cls, name, bases, cls_dict)

class MyClass(metaclass=MyMeta):
    x = 10
    def foo(self):
        pass

この定義で `MyClass` を作ると、`x = 10` のような書き込みが実行されるたびにメッセージが表示され、最終的に new で受け取る `cls_dict` の中身を確認できます。


プログラム的なクラス生成ステップ

通常の(宣言的)クラス定義では、これらはPythonが自動で行っています。大まかな流れは:

  1. 名前(例: `'MyClass'`)、ベースクラス(例: `(object,)`)、その他の引数を決定。

  2. `MyMeta.prepare(name, bases, ...)` を呼び出し → 辞書のようなオブジェクトが返る。

  3. 返ってきた辞書に module, qualname などのキーを注入。

  4. クラス本体(`class MyClass:` のブロック)を、先ほどの辞書を名前空間として実行。

  5. `MyMeta.new(MyMeta, name, bases, cls_dict, ...)` を呼び出し、最終的なクラスオブジェクトを生成。

  6. 生成されたクラスを `MyClass` に割り当て。

もし prepare を書かなければ、`type` のデフォルト実装が空の辞書を返します。


クラスの呼び出しを可能にする __call__

ここまででクラスの作られ方を見てきました。しかし、Pythonのクラスは「呼び出し可能(callable)」になっています。つまり `some_class(...)` と書くとインスタンスが作られます。実はこの呼び出しもメタクラスを通ります。具体的にはメタクラスの call が使われます。

インスタンス生成の仕組み

例えば:

p = Person('Guido')

とすると、Pythonは内部的にはほぼ次のように呼び出します:

Person.__class__.__call__(Person, 'Guido')
  • `Person.class はメタクラス(多くの場合 `type`)。

  • `type.call は(`Person` にバインドされた形で)呼び出される。すると以下を行う:

    1. `Person.new を呼んで新しい未初期化のオブジェクトを作る。

    2. その戻り値が正しく `Person` のインスタンスであれば、`Person.init を呼ぶ。

    3. 最後に初期化されたインスタンスを返す。

つまりクラスが呼び出せるのは、「クラスを生成したメタクラスが call を実装しているから」というわけです。もしメタクラスで call をオーバーライドすれば、トップレベルでのインスタンス生成ロジックに介入することができます。

クラスのクラス

`type` 自体もクラスなので、それにもメタクラスが存在します。それがまた `type` であり、いわば自己参照的な構造になっています。とはいえ、やっていることは同じで、`type(...)` と呼び出したときに `type.call → `type.new → `type.init と進むわけです。


すべてをまとめると

  1. prepare が最初に呼ばれ、初期状態のクラス辞書を得る。これをオーバーライドするとクラスの名前空間構築を自在にコントロールできる。

  2. クラス本体がその辞書の名前空間で実行され、Pythonは modulequalname などいくつかのキーを注入する。

  3. new が呼ばれ、最終的にクラスオブジェクトを返す。ここで追加のロジックや最終調整を行える。

  4. call はクラスオブジェクトを「呼び出し可能」にする仕組み。`type` のデフォルト実装では newinit → インスタンス返却、という流れを実装している。

この3つ—prepare, カスタム辞書の活用, call—は高度なPythonメタプログラミングの核となる概念です。こういった仕組みがあるからこそ、Pythonのオブジェクトモデルは非常に柔軟で、クラス形成やオブジェクト生成を自由にカスタマイズできるのです。

通常のアプリケーションコードでは、これらのフックを使う場面はめったにありませんが、ライブラリやフレームワークを作る際には非常に強力な手段となります。Pythonがクラスを生成するときに何をしているのか、どうやってクラスが呼び出し可能になるのかを知れば、必要なときに正確かつ洗練されたやり方で改変や拡張が可能になります。


「超温和なパイソン」へ

いいなと思ったら応援しよう!