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を継承したクラスが作成されるタイミングで呼ばれ、この中でクラスとテーブルのひもづけを行なう。
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_