見出し画像

Python 3: Deep Dive (Part 4 - OOP): Structs、Singleton、Config (セクション14-6/15)

  • SlottedStructメタクラス でフィールド定義だけでプロパティやメソッドを自動生成し、ボイラープレートを削減する。

  • Singletonパターン をメタクラスの `call` で制御し、すべてのインスタンス生成を一度きりにする。

  • ConfigTypeメタクラス で `.ini` ファイルを読み込み、環境別・セクション別にクラスを動的生成してドキュメントや使い勝手を向上させる。

もしあなたがPythonコードを見ながら「同じようなボイラープレートばかり書いてる!」あるいは「クラスの継承だけでは全部解決できないのか」と思ったことがあるなら、メタプログラミングは大きな助けになるかもしれません。メタクラスや低レベルフックを使うことで、コードの構造を変え、重複を取り除き、反復的なパターンを自動化できます。

以下では、メタプログラミングの3つの実用的な例を示します。

  1. DRY(重複排除)な「slotted struct」クラスを生成する。

  2. Singletonパターンをメタクラスで実装する。

  3. `.ini`ファイルを読み込んで動的なコンフィグシステムを構築する(環境とセクションごとにPythonクラスとして定義)。

これらの例はすべてメタクラスの仕組みに依存しますが、それぞれ異なる現場のニーズをカバーしています。


Slotted Structクラスでボイラープレートを減らす

問題:繰り返しのクラス定義

たとえば`Point2D`や`Point3D`など複数のクラスを用意し、以下を含む場合を考えましょう。

  • いくつかのフィールド(例:`x`, `y`、もしかしたら`z`)。

  • 各フィールドに対して読み取り専用のプロパティ(`x`は`_x`を返す、など)。

  • よくあるメソッド(`eq`, `hash`, `repr`, `str`など)。

  • メモリ節約のための`slots`。

クラスが増えるたびに同じコードを何度も書くことになり、重複が増えます。これこそボイラープレートの塊。

メタクラスによる解決策

ここでは、メタクラスを使って以下を自動化します。

  1. 宣言された`_fields`に基づいて`slots`を生成。

  2. 各フィールドに対して読み取り専用のプロパティを自動生成。

  3. 標準的なメソッド(`eq`, `hash`, `repr`, `str`)を注入。

以下は`SlottedStruct`メタクラスの中心部です。

class SlottedStruct(type):
    def __new__(cls, name, bases, class_dict):
        cls_object = super().__new__(cls, name, bases, class_dict)
        
        # __slots__を定義
        setattr(cls_object, '__slots__', [f'_{field}' for field in cls_object._fields])
        
        # 読み取り専用プロパティを生成
        for field in cls_object._fields:
            slot_name = f'_{field}'
            setattr(cls_object, field,
                    property(fget=lambda self, attrib=slot_name: getattr(self, attrib)))
        
        # __eq__ を生成
        def eq(self, other):
            if isinstance(other, cls_object):
                self_vals = [getattr(self, field) for field in cls_object._fields]
                other_vals = [getattr(other, field) for field in cls_object._fields]
                return self_vals == other_vals
            return False
        setattr(cls_object, '__eq__', eq)

        # __hash__ を生成
        def hash_(self):
            field_values = (getattr(self, field) for field in cls_object._fields)
            return hash(tuple(field_values))
        setattr(cls_object, '__hash__', hash_)

        # __str__ を生成
        def str_(self):
            field_vals = [str(getattr(self, field)) for field in cls_object._fields]
            return f'{cls_object.__name__}({", ".join(field_vals)})'
        setattr(cls_object, '__str__', str_)

        # __repr__ を生成
        def repr_(self):
            kv_pairs = [f'{field}={getattr(self, field)}' for field in cls_object._fields]
            return f'{cls_object.__name__}({", ".join(kv_pairs)})'
        setattr(cls_object, '__repr__', repr_)

        return cls_object

使い方

こうして、クラスは`_fields`を宣言するだけで済みます。

class Point2D(metaclass=SlottedStruct):
    _fields = ['x', 'y']
    def __init__(self, x, y):
        self._x = x
        self._y = y

class Point3D(metaclass=SlottedStruct):
    _fields = ['x', 'y', 'z']
    def __init__(self, x, y, z):
        self._x = x
        self._y = y
        self._z = z
  • `slots`は自動的に生成され(例:`['_x', '_y']`、または`['_x', '_y', '_z']`)。

  • `x`, `y`(および`z`)の読み取り専用プロパティを得られます。

  • `eq`, `hash`, `str`, `repr`が自動的に注入されます。

2次元や3次元のポイント以外にも、「フィールドの定義+`init`」だけで完全装備のクラスが手に入るわけです。

デコレータでラップする

もし毎回`metaclass=SlottedStruct`と書きたくないなら、クラスデコレータを用いて `SlottedStruct(...)` を呼ぶ方法もあります。コード量削減になりますが、メタクラスを使っていることが表からは見えにくくなるというデメリットもあります。


メタクラスでSingletonを実装

パターン:1クラス1インスタンス

Singletonは、いかなる呼び出しをしても常に同じインスタンスを返す特別なクラスです。Pythonの`None`、`True`、`False`などがそう。場合によっては1つだけ必要なグローバル状態を持つクラスに使うかもしれません(ただしSingletonの是非は議論が多いのも事実)。

単純な実装

メタクラスなしで実装するときは、クラスの`new`をオーバーライドして、以下のようにインスタンスをキャッシュできます。

class Hundred:
    _existing_instance = None
    def __new__(cls):
        if not cls._existing_instance:
            print('creating new instance...')
            instance = super().__new__(cls)
            instance.name = 'hundred'
            instance.value = 100
            cls._existing_instance = instance
        else:
            print('using existing instance...')
        return cls._existing_instance

しかし多くのSingletonクラスを作ったり、継承に対応しようとすると、コード重複が増えがちです。

メタクラスアプローチ

より簡潔かつパワフルなのは、メタクラスの `call` をオーバーライドする方法です。クラスを呼び出してインスタンスを作るとき、実は`クラスのメタクラスの__call__`が呼ばれます。

class Singleton(type):
    instances = {}
    def __call__(cls, *args, **kwargs):
        print(f'Request to create instance of {cls}...')
        existing_instance = Singleton.instances.get(cls, None)
        if existing_instance is None:
            print('Creating instance for the first time...')
            existing_instance = super().__call__(*args, **kwargs)
            Singleton.instances[cls] = existing_instance
        else:
            print('Using existing instance...')
        return existing_instance

このメタクラスを使うときは:

class Hundred(metaclass=Singleton):
    value = 100

class Thousand(metaclass=Singleton):
    value = 1000
  • 最初の呼び出しで「Creating instance for the first time...」。

  • 2回目以降は「Using existing instance...」。

さらに継承関係も、クラスごとに`instances`辞書で別々に管理されます。


`.ini`ファイルを使ったConfigの動的生成

目的

プロダクションやデベロップメントなど、環境別に`.ini`ファイルで設定を管理したいとき、たとえば `dev.ini` は:

[Database]
db_host=dev.mynetwork.com
db_name=my_database

[Server]
port=3000

`prod.ini` は本番向けの値が書かれる。
そしてPython側で:

DevConfig.database.db_host  # → "dev.mynetwork.com"
ProdConfig.server.port      # → "8080" など

のようにドット表記でアクセスし、`help(DevConfig)`で何があるか分かると便利です。
単に`configparser`を使ってインスタンスにロードするだけでは、セクション構造が曖昧になり、`help()`で何もわからないというデメリットがありました。

セクション用メタクラス: `SectionType`

まず、`.ini`の各セクションをPythonクラス化する仕組みを用意します。

import configparser

class SectionType(type):
    def __new__(cls, name, bases, cls_dict, section_name, items_dict):
        cls_dict['__doc__'] = f'Configs for {section_name} section'
        cls_dict['section_name'] = section_name
        for key, value in items_dict.items():
            cls_dict[key] = value
        return super().__new__(cls, name, bases, cls_dict)

例: 手動で生成

class DatabaseSection(metaclass=SectionType,
                      section_name='Database',
                      items_dict={'db_host': 'somehost', 'db_name': 'some_db'}):
    pass

これで`DatabaseSection.db_host`や`DatabaseSection.db_name`がクラス属性として作られ、`help(DatabaseSection)`にも明示されます。

動的生成

型生成用に `MySection = SectionType(...)` のように呼び出してクラスを作ることも可能。
本来は、どのセクションがあるか分からない`.ini`を読みながら複数のセクション用クラスを動的に作りたいので、宣言的に書くのではなくプログラムで呼び出します。

Config用メタクラス: `ConfigType`

全体のConfigクラスを作るメタクラスです。環境を受け取り、該当の`.ini`ファイルを読んでセクションごとに`SectionType`を呼び出してクラスを構築し、`Config`クラスのクラス属性に登録します。

class ConfigType(type):
    def __new__(cls, name, bases, cls_dict, env):
        cls_dict['__doc__'] = f'Configurations for {env}.'
        cls_dict['env'] = env
        
        config = configparser.ConfigParser()
        config.read(f'{env}.ini')
        
        for section_name in config.sections():
            class_name = section_name.capitalize()
            class_attribute_name = section_name.casefold()
            section_items = config[section_name]
            bases = (object,)
            section_cls_dict = {}
            # セクション用のクラスをSectionTypeで生成
            Section = SectionType(class_name, bases, section_cls_dict,
                                  section_name=section_name,
                                  items_dict=section_items)
            # Configクラスの属性として格納
            cls_dict[class_attribute_name] = Section
        
        return super().__new__(cls, name, bases, cls_dict)

デベロップ・本番用のConfigクラス

以下のように書くだけで、それぞれの`.ini`を読み込み、セクションごとにクラスを自動生成します。

class DevConfig(metaclass=ConfigType, env='dev'):
    pass

class ProdConfig(metaclass=ConfigType, env='prod'):
    pass
DevConfig.database.db_host   # dev.mynetwork.com
ProdConfig.server.port       # 8080
help(DevConfig)             # "Configurations for dev." と表示
help(DevConfig.database)     # "Configs for Database section" が表示

モジュールとして読み込んだ時点で`DevConfig`や`ProdConfig`クラスが作られ、何度インポートしても再読み込みされないため、`.ini`ファイルの読み込みが複数回走ることもありません。`help`でもセクション名やキーをはっきり確認できます。


まとめ

  • Struct的クラス生成
    フィールド定義や各種メソッドを書くボイラープレートをなくし、`_fields`を宣言すれば自動的に`slots`やプロパティ、`eq`, `hash`, `repr`, `str`などを注入。

  • Singletonパターン
    クラスの`call`をメタクラスでフックして、インスタンス生成を1回きりに制御。

  • Config読み込み
    `.ini`ファイルを読み取り、環境(devやprodなど)ごと、セクションごとにクラスを生成。`help()`などのドキュメント生成も自然に行える。

メタクラスは強力で、高度な機能です。使い所を誤ると可読性が損なわれる可能性はありますが、うまく活用すれば繰り返しが多い構造をスマートに解決し、最終的なコードは明瞭かつ楽しいものに仕上がります。慎重かつ賢く使ってみてください。


「超温和なパイソン」へ

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