Pythonのメタクラス:高度なオブジェクト指向プログラミング

メタクラスとは

オブジェクト指向プログラミング(OOP)において、異なるクラスを使って様々なエンティティと操作を記述できます。親クラスを通じて「デフォルト」の操作を設計したり、MixInクラスで追加機能を組み合わせたり、抽象クラスや抽象メソッドで実装すべきインターフェースを定義したりすることが可能です。 多くの場合、メタクラスは必要ありません。

メタクラスはtype(typeのサブクラス)であり、カスタムタイプとしてクラスの呼び出し、オブジェクト作成、初期化、破棄などの操作をカスタマイズできます。

メタクラスの使用シナリオ

多くの場合、メタクラスは通常のクラスに制約と規則を適用するために使用されます。メタクラス(カスタムタイプ)を使用すると、クラスの作成(__new__)、初期化などにおいて、特定の属性の存在や特定のメソッドの実装を強制したり、クラスが作成できるインスタンスオブジェクトを1つに制限したりできます。 典型的な使用シナリオは以下の通りです:

  • クラスのインスタンス化を禁止
  • シングルトンパターン:各クラスで1つのオブジェクトのみ作成を許可
  • 属性に基づくオブジェクトのキャッシュ:特定の属性が同じ場合に同一オブジェクトを返す
  • 属性制限:クラスに特定の属性またはメソッドの実装が必要
  • ORMフレームワーク:データベーステーブルをクラスとクラス属性で記述し、データベース操作に変換

メタクラス使用例

クラスのインスタンス化を禁止

特定のクラスでオブジェクトの作成を禁止し(クラス名での操作のみ許可)、メタクラスで制限を加えることができます。以下にコード例を示します。

class InstanceBlocker(type):  # メタクラスの定義 - typeを継承
    def __call__(self, *args, **kwargs):  # クラス呼び出し(インスタンス化)プロセスを制御
        """クラス呼び出し"""
        raise TypeError("インスタンス化は許可されていません")


class Configuration(metaclass=InstanceBlocker):  # メタクラスの使用宣言
     pass

config = Configuration()  # ()はクラスの__call__操作を呼び出すため、例外が発生し直接インスタンス化できません

シングルトンパターン

クラスで1つのインスタンスオブジェクトのみ作成を許可する場合も、メタクラスで制限できます。以下にコード例を示します:

class UniqueInstance(type):   # シングルトンタイプ - カスタムメタクラス
    def __init__(self, *args, **kwargs):
        self._singleton_object = None   # 唯一のインスタンスオブジェクトを保存するためのプライベート属性
        super().__init__(*args, **kwargs)

    def __call__(self, *args, **kwargs):  # クラス呼び出しの制御
        if self._singleton_object is None:
            self._singleton_object = super().__call__(*args, **kwargs)  # 存在しなければ作成
            return self._singleton_object
        else:
            return self._singleton_object    # すでに存在する場合は返却


class DatabaseConnection(metaclass=UniqueInstance):
    def __init__(self):
        print('データベース接続を作成')

conn1 = DatabaseConnection()  # オブジェクト作成
conn2 = DatabaseConnection()  # オブジェクト作成
print(conn1 is conn2)  # Trueを返却、両者は同一オブジェクト

属性に基づくオブジェクトのキャッシュ

これはシングルトンパターンの拡張で、特定の属性に対して、完全に一致する属性の組み合わせで同一オブジェクトを作成します。

import weakref

class AttributeCache(type):
   def __init__(self, *args, **kwargs):
       super().__init__(*args, **kwargs)
       self._cache = weakref.WeakValueDictionary()  # キャッシュ辞書を追加

   def __call__(self, *args):
       if args in self._cache:   # パラメータの組み合わせでキャッシュ辞書を検索
           return self._cache[args]   
       else:
           obj = super().__call__(*args)  # オブジェクトを作成
           self._cache[args] = obj  # パラメータの組み合わせ(タプル型)をキャッシュ辞書に保存
           return obj


class UserProfile(metaclass=AttributeCache):
   def __init__(self, username):
       print('ユーザープロファイル({!r})を作成'.format(username))
       self.username = username

profile_a = UserProfile('john_doe')
profile_b = UserProfile('jane_doe')
profile_c = UserProfile('john_doe')
print(profile_a is profile_b)  # False ユーザー名が異なるため同一オブジェクトではない
print(profile_a is profile_c)  # True ユーザー名が同じため同一オブジェクト

クラスに特定の属性の存在を要求

class ValidationMeta(type):
    def __new__(cls, name, bases, attrs):
        print('クラス名', name)
        print('基底クラス', bases)
        print('属性', attrs)
        required_attributes = {'priority', 'timeout', 'owner', 'status', 'execute_test'}
        if required_attributes - set(attrs.keys()):
            raise TypeError('テストクラスはpriority、status、owner、timeout属性を実装し、execute_testメソッドを含める必要があります')
        return super().__new__(cls, name, bases, attrs)


class TestB(metaclass=ValidationMeta):
    # priority = 'P1'  # この属性をコメントアウトすると、インスタンス化時にエラーが発生します
    timeout = 10
    owner = 'developer'
    status = 'ready'
    
    def execute_test(self):
        pass


test = TestB()  # ここでテストクラスのpriority属性をコメントアウトしているため、インスタンス化時にエラーが発生します

シンプルなORMフレームワーク

ORM(Object-Relational Mapping)はオブジェクトとリレーショナルデータベース間のマッピング技術で、オブジェクト指向の方法でデータベースを操作できます。DjangoのORMモデルやSQLAlchemyはメタクラスに基づいて実装されており、データベース操作をクラス宣言とオブジェクト操作にマッピングします。以下はメタクラスに基づいたシンプルなORMフレームワークの実装例です。

class ORMBase(type):  # メタクラス
    def __new__(cls, name, bases, attrs):
        if name == 'Model':
            return super().__new__(cls, name, bases, attrs)

        table_name = attrs.get('__table__', name.lower())  # クラスにtable_name属性が含まれている場合はテーブル名として使用
        field_mappings = {}
        field_list = []
        primary_key_field = None

        for field_name, field_value in attrs.items():
            if isinstance(field_value, DatabaseField):
                field_mappings[field_name] = field_value
                if field_value.primary_key:
                    if primary_key_field:
                        raise RuntimeError('重複したプライマリキー: {}'.format(field_name))   # プライマリキーは1つのみ許可
                    primary_key_field = field_name
                else:
                    field_list.append(field_name)

        if not primary_key_field:
            raise RuntimeError('テーブル {} のプライマリキーが見つかりません'.format(table_name))   # プライマリキーがない場合はエラー

        for key in field_mappings.keys():
            attrs.pop(key)

        attrs['__table__'] = table_name
        attrs['__field_mappings__'] = field_mappings
        attrs['__field_list__'] = field_list
        attrs['__primary_key__'] = primary_key_field

        return super().__new__(cls, name, bases, attrs)

class Model(metaclass=ORMBase):  # データモデル - データベーステーブルに対応
    def __init__(self, **kwargs):
        for key, value in kwargs.items():
            setattr(self, key, value)

    def save(self):  # オブジェクト保存メソッド - データベーステーブルへの挿入操作に対応
        fields = []
        placeholders = []
        parameters = []

        for key, value in self.__field_mappings__.items():
            if value.primary_key:
                continue
            fields.append(value.name)
            placeholders.append('?')
            parameters.append(getattr(self, key, None))

        sql = 'INSERT INTO {} ({}) VALUES ({})'.format(self.__table__, ','.join(fields), ','.join(placeholders))
        print('SQL:', sql)
        print('パラメータ:', parameters)

class DatabaseField:  # データベースフィールド
    def __init__(self, name, column_type, primary_key=False):
        self.name = name
        self.column_type = column_type
        self.primary_key = primary_key

    def __str__(self):
        return '<{}:{}>'.format(self.__class__.__name__, self.name)

class CharField(DatabaseField):  # 文字列型フィールド - varcharに対応
    def __init__(self, name, primary_key=False):
        super().__init__(name, 'varchar(100)', primary_key)

class IntField(DatabaseField):   # 整数型フィールド - bigintに対応
    def __init__(self, name, primary_key=False):
        super().__init__(name, 'bigint', primary_key)

この例では、ORMBaseメタクラスを定義し、Modelを継承するクラスを作成しています。このメタクラスでは、まずクラスで定義されたすべてのフィールドを取得し、__field_mappings__辞書に保存します。同時に、プライマリキーフィールドを取得し、__primary_key__属性に保存します。

Modelクラスでは、オブジェクトの属性を初期化する__init__メソッドと、オブジェクトをデータベースに保存するsaveメソッドを定義しています。このメソッドでは、まずすべての非プライマリキーフィールドをfieldsリストに保存します。次に、placeholdersリストでプレースホルダーを保存し、parametersリストでパラメータを保存します。最後に、これらの情報を使用してSQLステートメントを構築し、出力します。

DatabaseFieldクラスでは、フィールドの名前とタイプを表示する__str__メソッドを定義しています。また、CharFieldIntFieldの2つのサブクラスを定義し、それぞれ文字列型と整数型のフィールドを表します。

このORMフレームワークを使用すると、Modelを継承するクラスを定義し、その中にフィールドを定義できます。例えば:

class Customer(Model):
    id = IntField('id', primary_key=True)
    name = CharField('customer_name')
    email = CharField('email_address')
    age = IntField('customer_age')

その後、Customerオブジェクトを作成し、データベースに保存できます:

customer = Customer(id=1, name='Bob', email='bob@example.com', age=30)
customer.save()

この例はシンプルなORMフレームワークですが、実際のORMフレームワークはクエリ、更新、削除などの機能もサポートする必要があります。しかし、この例を通じて、メタクラスを使用してORMフレームワークを作成する方法を理解できます。

自動登録と親クラスの継承

class AutoInheritance(type):
    registered_parents = []

    def __new__(cls, name, bases, attrs):
        bases = tuple(list(bases) + cls.registered_parents)
        return super().__new__(cls, name, bases, attrs)

    @classmethod
    def register_parent(cls, parent_class):
        cls.registered_parents.append(parent_class)


def register_decorator(parent_class):
    AutoInheritance.register_parent(parent_class)


@register_decorator
class BaseFeature:
    pass


class MainComponent(metaclass=AutoInheritance):
    pass


print(MainComponent.__bases__)

参考:python3-cookbook メタクラスを使用したインスタンス作成の制御

タグ: Python メタクラス オブジェクト指向 シングルトン ORM

6月8日 23:30 投稿