2024年2月20日火曜日

Python で動的にクラスを定義する方法

Python で動的にクラスを定義する方法

概要

Python でクラスの定義を関数で行ってみます
サンプルコードとメリットなどを紹介します

環境

  • macOS 11.7.10
  • Python 3.11.6
    • pyright 1.1.311

基本的な使い方

クラスは type を使って定義します
コンストラクタや属性、メソッドはすべて dict 形式で指定します

メソッドの場合ラムダ式を使って定義することもできます

def init(self, name="hawk", age=10):
    setattr(self, "name", name)
    setattr(self, "age", age)


def show_with_newline(self):
    print(self.name)
    print(self.age)


User = type(
    "User",
    (object,),
    {
        # コンストラクタを定義する
        "__init__": init,
        # クラスの属性を定義する、今回はコンストラクタで属性の初期化をしているが以下のコメントを外せばここでも属性の初期値を設定することは可能
        # "name": "",
        # "age": 10,
        # メソッドの定義 (ラムダでも定義できる
        "show": lambda self: print(f"name -> {self.name}, age -> {self.age}"),
        # メソッドの定義 (関数名を指定する
        "show_with_newline": show_with_newline,
    },
)

u = User()
u.show()

u = User("hawksnowlog", 20)
u.show()
u.show_with_newline()

デメリット

  • pyright でエラーが発生する
    • User クラスに show メソッドはありません
    • User クラスのコンストラクタには引数を指定することはできませんなど
  • 当然 pyright で補完などが発生しない
  • コードの可読性が下がる
    • 一般的なクラス定義の記述ではなくなる
    • 後述する関数化などをした場合にはどんなクラスがいくつ作られているのかコードからは認識できなくなる

動的に作成したクラスを使ってタイプヒントを使う

こんな使い方もできます (この場合でも pyright のエラーがでます

def init(self, name="hawk", age=10):
    setattr(self, "name", name)
    setattr(self, "age", age)


def show_with_newline(self):
    print(self.name)
    print(self.age)


User = type(
    "User",
    (object,),
    {
        # コンストラクタを定義する
        "__init__": init,
        # クラスの属性を定義する、今回はコンストラクタで属性の初期化をしているが以下のコメントを外せばここでも属性の初期値を設定することは可能
        # "name": "",
        # "age": 10,
        # メソッドの定義 (ラムダでも定義できる
        "show": lambda self: print(f"name -> {self.name}, age -> {self.age}"),
        # メソッドの定義 (関数名を指定する
        "show_with_newline": show_with_newline,
    },
)


def create_user() -> User:
    return User()


user = create_user()
user.show()

関数化する

クラス定義を生成する関数を定義します
こうすることでいろいろなクラスを動的に生成することができます

この場合は pyright で「Illegal type annotation: variable not allowed unless it is a type alias」が発生します

from typing import Any, Callable


def init(self, name="hawk", age=10):
    setattr(self, "name", name)
    setattr(self, "age", age)


def show_with_newline(self):
    print(self.name)
    print(self.age)


def factory_class(
    class_name: str, attributes: dict[str, Any], funcs: dict[str, Callable]
) -> type:
    return type(
        class_name,
        (object,),
        dict(**attributes, **funcs),
    )


User = factory_class(
    "User", {"name": "hawk", "age": 10}, {"__init__": init, "show": show_with_newline}
)


def create_user() -> User:
    return User()


user = create_user()
user.show()

最後に

Python で動的にクラスを生成する方法を紹介しました
クラスの定義を dict などで保持することができるのでデータドリブンなプログラミングができるようになります
ただメタプログラミングなのでコードの可読性や lsp との相性が悪くなるのでそのあたりのトレードオフを考慮する必要がありそうです

参考サイト

0 件のコメント:

コメントを投稿