5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

pythonのメタクラスで、ORMのメソッドに比較演算子を文字列として渡して、where句を組み立てる。

Last updated at Posted at 2015-12-14

ORMマッパーを自作しているのですが、whereメソッドに比較演算子を引数としてそのまま渡す処理を実装しました。次のような使い方をイメージしてもらえると良いです。より直感的にwhere句の各条件を指定できます。

Session.select('coulmn_name1', 'coulmn_name2').where(Model.id > 1, _and, Model.name == "Mike")

Model.id > 1,は引数として渡すと通常ではTrueかFalseが渡されてしまいidが1より大きいという意味を渡すことができません。しかし、特殊メソッドを書き換えれば比較演算子として処理させずに、引数を渡す事ができます。

今回のwhereメソッドでは、「文字列としてwhere句を返す」という処理だけを実装しています。やることは単純ですが、この実装を知ることによってメタプログラミングに便利なことがわかりました。

  • メタプログラミングのメリットがわからない
  • メタプログラミングはわかったけど、何に使えばいいの?
  • メタプロの意味って?

というような人にも有益な情報が得られるかなと思います。ただ、メタプログラミング自体の解説はこの記事では行いません。メタプログラミングがわかっている人が、実例を知りたい場合にこの記事が役立つと思います。

自作モデルの作り方

Personという人を扱うテーブルが有る場合は次のようにPersonモデルをクラスで宣言します。

from model_base import *

class Person(ModelBase):
    id = {'default': 1}
    name = {'default': 'Mike'}

モデルには必要なカラム名を宣言するだけで使用することができます。初期値にはオプションを辞書で指定します。今回はdefalutオプションしか用意していません。ModelBaseさえ継承すれば、後は自動でモデルを組み立ててくれます。

Personクラスが読み込まれた際には、クラスフィールド、idnameには{'default': 1}のような辞書は入っておりません。メタクラスによって自動でColumnクラスのオブジェクトが入っています。defaultを見たい場合は、pseron_instance.id.defaultと記述すればいいです。

カラム名として扱われる変数名

クラスフィールドには先頭に_(アンダーバー)を除いたカラム名を宣言します。接頭辞に_(アンダーバー)を使用した場合はカラムと認識されないため、必要なメソッドやフィールドはプライベートとして_(アンダーバー)を最初につけてください。

実際に使ってみる

今回はwhereメソッドでは組み立てたwhere句を文字列で返すようにしています。

from person import *

if __name__ == '__main__':
    db_manager = DBManager()

    # where句の組み立て
    db_manager.where(Person.id > 1, "or", Person.name == 'Mike')
    # ローカル変数で用意するとタイプが楽
    _and = "and"
    _or = "or"
    db_manager.where(Person.id > 1, _and, Person.name == 'Mike')

    # デフォルトの確認
    person_A = Person()
    print("person_A.id = %s" % person_A.id)
    print("person_A.name = %s" % person_A.name)

    # Model.idにはCoulmオブジェクト、model.idにはカラム値が入っているのでエラー
    person_A = "Jane"
    print(Person.name.default)
    # print(person_A.name.default)
    # => AttributeError: 'str' object has no attribute 'name'

モデルを組み立てるモジュール

用意するクラスは次の4点です。

  • カラム
  • モデルのベース
  • モデルのベースを作るメタクラス
  • SQLを実行するマネージャークラス
class Column():

    def __init__(self, column_name, dict):
        # カラムの名前をクラス変数で保持する
        self.column_name = column_name

        # 各カラムの設定値を受け取る場合はセットする
        if dict is not None:
            for key, value in dict.items():
                if key == "default":
                    self.default = value

    def __setattr__(self, key, value):
        # valueをチェックしてエラーを投げればカラムの型チェックができる
        self.__dict__[key] = value

    # 比較演算子をオーバーロード
    def __eq__(self, other):
        return "%s = %s" % (self.column_name, other)

    def __ne__(self, other):
        return "%s != %s" % (self.column_name, other)

    def __lt__(self, other):
        return "%s < %s" % (self.column_name, other)

    def __gt__(self, other):
        return "%s > %s" % (self.column_name, other)

    def __le__(self, other):
        return "%s <= %s" % (self.column_name, other)

    def __ge__(self, other):
        return "%s >= %s" % (self.column_name, other)

class MetaModel(type):
    def __new__(cls, cls_name, cls_bases, cls_dict):
        for key, value in cls_dict.items():
            # publicな属性のみ取り出す
            if not(key.startswith("_")):
                # カラム(Modelのクラス変数)の初期値を取り出すことができる
                # 変数名をColumnのフィールドで保持
                cls_dict[key] = Column(key, value)
        return super().__new__(cls, cls_name, cls_bases, cls_dict)

class ModelBase(metaclass=MetaModel):
    # インスタンスを生成した直後に呼び出される
    def __init__(self):
        # Columnオブジェクトに設定されたデフォルト値をセット
        class_dict = self.__class__.__dict__
        for column_key, column_value in class_dict.items():
            # publicな属性のみ取り出す
            if not(column_key.startswith("_")):
                setattr(self, column_key, column_value.default)

class DBManager():
    def where(self, *args):
        statement = "WHERE"
        for arg in args:
            if arg.upper() in ["AND", "OR"]:
                statement += " %s " % arg.upper()
            else:
                statement += " %s " % arg
        print(statement)

メタクラスはModelBaseに設定しました。これはサブクラスでmetaclass=MetaModelと記述せずに、MetaModelとだけ記述すれば済むようにするためです。また、モデルの共通処理はここに記述します。

各カラムの情報は専用のクラスを用意して、そのインスタンスで管理しています。こうすることによって各カラムにバリデートなどの自動処理を実装できます。もし、専用のクラス、Columnクラスがなければ、カラム値の受け渡ししか行うことができません。

# ModelBaseがない場合
class MyModel(metaclass=MetaModel):
    pass

# ModelBaseがある場合
class MyModel(ModelBase):
    pass

Personのインスタンス生成しなくても、Personを読み込んだ時点でPersonのメタクラスであるModelMetaが実行されます。そのため、Personのクラスフィールドであるidやnameに動的にColumnオブジェクト(インスタンス)が生成されてセットされます。このColumnにはそれぞれのデフォルト値をインスタンスメンバのdefaultに保持しています。

PersonのクラスフィールドにColumnオブジェクトがセットされる流れ

  1. Personクラスを読み込む
  2. MetaModelが実行される
  3. Personのクラスフィールドの初期値なる予定だった値を受け取る 例:{"default": 1}
  4. {"default" : 1}を引数にColumnオブジェクトを生成
  5. クラスの属性一覧であるdictにColumnオブジェクトを登録
  6. MetaModelの親であるメタクラスtypeにクラスフィールドの定義を任せる
  7. メタクラスtypeがColumnオブジェクトをPersonのクラスフィールドにセット

メタクラスとクラスのnewの違い

MetaModelというメタクラスは、メタクラスにせずに通常のクラスとして定義することもできます。その場合はModelBaseだけにすることもできるでしょう。ただ、その場合は一度インスタンスを作らないといけません。モジュールを読み込んだ時に自動実行されるように、ソースコードの一番最後にインスタンス生成を書いておけばいいと思います。でも、あまりいいやり方では無い気がします。

class Column():
    pass

class MetaModel(type):
    pass

class DBManager():
    pass

class ModelBase():
	pass

ModelBase()`

わざわざ一度インスタンスを生成する必要があるのは、__new__がインスタンスを生成する処理を行うからです。(厳密には直前です。生成は別に任せています。)ここで大事なのは、何のインスタンスかということです。メタクラスはクラスを生成するためのクラス、つまりメタクラスのインスタンスがクラスになります。一方、ただのクラスのインスタンスは通常の普段使っているクラスから生成されたオブジェクトです。通常、インスタンスというとクラスの生成物を指しますが、関係性を取り上げるとメタクラスとクラスの中でもインスタンスという表現を使うことが出来ます。

次の2つの関係性はどちらも設計図と生成されるものです。右側はインスタンスと見ることが出来ます。

  • メタクラス ←→ クラス
  • クラス ←→ インスタンス

これを踏まえて__new__がインスタンスをする際に実行するものだと見えれば、メタクラスとクラスの__new__のシンプルな違いは処理の順番になります。最後に実行される順番を整理して終わりにします。

実行される順番

  1. メタクラス.__new__
  2. クラスが作成される
  3. クラス.__new__
  4. インスタンスが生成される
  5. クラス.__init__
  6. インスタンス変数がセットされる
5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?