LoginSignup
20
16

More than 5 years have passed since last update.

pythonのメタクラスとsqlalchemyのdeclarative

Posted at

pythonのメタクラスとsqlalchemyのdeclarative

メタクラスはライブラリなどでないかぎり使用する機会は少ないが、SQLAlchemyなどでは使用されているので、参考程度にまとめる。

メタクラスとは、クラスの型。pythonの場合、ユーザー定義クラスの型は基本的にはtypeになる。

>>> class A(object):
>>>     pass
>>>
>>> type(A)
type

特殊な用途のために、type以外のメタクラスを使用することがある。

メタクラスの身近な用途としては、SQLAlchemyのdeclarativeの中でこれが使われている。

>>> from sqlalchemy.ext.declarative import declarative_base
>>> Base = declarative_base()
>>> type(Base)
<class 'sqlalchemy.ext.declarative.api.DeclarativeMeta'>

declarativeを使用すると、クラス定義が自動的にテーブル定義とひもづく。

class User(Base):
    __tablename__ = 'user_account'

    id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(Varchar(100))

内部的には、sqlalchemy.ext.declarative.api.DeclarativeMetaという独自のメタクラスを使用し、クラス定義時の処理をフックしてマッピングの処理を行なっている。

sqlalchemy.ext.declarative.api.DeclarativeMetaの定義は以下のようになっている。initはBaseを継承したクラスが作成されるタイミングで呼ばれ、この中でクラスとテーブルのひもづけを行なう。

sqlalchemy/ext/declarative/api.py
class DeclarativeMeta(type):
    def __init__(cls, classname, bases, dict_):
        if '_decl_class_registry' not in cls.__dict__:
            _as_declarative(cls, classname, cls.__dict__)
        type.__init__(cls, classname, bases, dict_)

    def __setattr__(cls, key, value):
        _add_attribute(cls, key, value)

pythonでは、クラスの定義から自動的に何かの処理を行ないたい場合などにメタクラスを使用するのは比較的よく用いられる手法らしい。例えばwtformsのFormクラスもほぼ同様のことをやっている。

ユースケースによってはあえてこの処理を止めたいこともある。DBからリフレクションで作成したTableオブジェクトとdeclarative_baseで作成したクラスを紐づけるといったユースケースを考えよう。こうした場合、以下のようなコードを利用できる。

from sqlalchemy import MetaData, Table
from sqlalchemy.ext.declarative import api, declarative_base
from sqlalchemy.ext.declarative.api import DeclarativeMeta


class_registry = {}
metadata = MetaData()
Base = declarative_base(class_registry=class_registry,
                        metadata=MetaData())


class MyMetaClass(DeclarativeMeta):
    def __init__(cls, classname, bases, dict_):
        # テーブルとの紐づけをしない
        type.__init__(cls, classname, bases, dict_)

    @classmethod
    def create_class(cls, table_name, class_name, dbsession):
        tableobj = Table(table_name, metadata, autoload=True,
                         autoload_with=dbsession.bind)
        class_ = MyMetaClass(class_name, (Base), {})
        # 以下で明示的にテーブルと関連づける。
        class_.__tablename__ = tablename
        class_.__table__ = tableobj
        for c in tableobj.columns:
            setattr(class_, c.name, c)
        api.instrument_declarative(class_, class_registry, metadata)
        return class_
20
16
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
20
16