2013年8月20日火曜日

SQLAlchemy を使ってメタプログラミングを語りき

概要
 前回に引き続き SQLAlchemy を使って python 上で SQL のテーブルを司るクラスを動的に生成することで、簡単に実装する方法を提案する

対象者
  危険なコードを含むので python 初心者、初級者は対象外。中級もしくは、これが商品や公開コードには用いるべきでないとわかるレベル。


背景
 pythonのORMを色々試してみた をはじめとして、 SQLAlchemyを使って動的に SQLを読み込む方法は書かれているが、予めテーブルの定義などが必要となっており正直めんどくさい。動的に定義自体も行う仕組みを作ることで、コード量を減らしたい



準備
  1. 環境は windows7, pythonは2.7.3
  2. python に sqlalchemy(0.8.2)を pip か easy_install なりを使って入れておく
  3. 前回のSQL ファイル("test.sql")をsqliteに書いたDB alembic_test.db をローカルに作る

 test.sql
DROP TABLE IF EXISTS REFERENCE;
CREATE TABLE PACAKGE(
    _ID INTEGER NOT NULL UNIQUE PRIMARY KEY AUTOINCREMENT,
    NAME TEXT NOT NULL UNIQUE,
    OWNER TEXT
)


 実装

説明するより物を見たほうが早い

 test.py 
# -*- coding: utf-8 -*-
from sqlalchemy.engine import create_engine
from sqlalchemy import MetaData, Table
from sqlalchemy.orm import mapper, sessionmaker

# point 1
from sqlalchemy.interfaces import PoolListener
class SetTextFactory(PoolListener):
    def connect(self, dbapi_con, con_record):
        dbapi_con.text_factory = str
engine = create_engine('sqlite:///alembic_test.db', listeners=[SetTextFactory()])
metadata = MetaData(bind=engine)
metadata.reflect()
session = sessionmaker(bind=engine)()

# point 2
classref = {"__init__": lambda x, **y : (lambda x: None) \
    ( [ x.__dict__.setdefault(y1,y2) for y1, y2 in y.items() if y1 not in x.__dict__ ] ) }

# point 3
for t in metadata.sorted_tables:
    globals()[t.name]= type(str(t.name), (object,), classref)
    try:
        mapper(globals()[t.name], Table(t.name, metadata, autoload=True))
    except:
        pass

# point 4
pacakge=PACAKGE(NAME="candy", OWNER="Tom")
session.add(pacakge)
pacakge=PACAKGE(NAME="チョコレート", OWNER="田中")
session.add(pacakge)
pacakge=PACAKGE(NAME="cake", OWNER="Sully")
session.add(pacakge)
session.commit()

package= session.query(PACAKGE).order_by(PACAKGE._ID.desc()).first()
print package.NAME, package.OWNER

session.close()
point 1
半分以上がおまじないに近い。
SetTextFactory で
dbapi_con.text_factory = str
とやっているのは、こうしないと データベース が日本語を受け付けないため。
あとは、DBファイルを開いてアクセス用のコネクションを張っているだけ。

point 2
 python に慣れていない人にはつらいものが多いと思われる。
classref  は辞書型のオブジェクトだが、point 3 で使用する関数 __init__() を定義している。 __init__() 内部は lamda * 2 とリストの内包表記 (list comprehension)を組み合わせている。

もともとは、以下の様な

 def __init__(x, **y):
    for y1, y2 in y.items():
        if y1 in x.__dict__:
            x.__dict__[y1]=y2
    return None  

初期化で渡された辞書情報 y をオブジェクト x のフィールドに登録する仕組み。
これを一行に書いているだけなのだ。

 forブロックを一つにまとめると
 def __init__(x, **y):
    [ x.__dict__.setdefault(y1,y2) for y1, y2 in y.items() if y1 not in x.__dict__ ]
    return None  
とリストの内包表記を使うこととなる。 リストの内包表記は "=" が使えないため seddefault() を使うところが工夫といえば工夫。更に lamda を一回はさむと

 def __init__(x, **y):
    (lambda x: None)([ x.__dict__.setdefault(y1,y2) for y1, y2 in y.items() if y1 not in x.__dict__ ])

となる。要は、Noneを返す関数を動的に作成している。そして引数にリスト内包表現を使う。通常、関数を呼ぶときは引数の式を評価してから動作することを利用している。最適化されたら無視されて動作しなくなるのだろうかと不安になるのは正しい反応。このように無理やり式を評価させつつ None を返す方法としては、以下の方法もあるが見栄えが悪い

None if map(lambda (y1, y2) :  x.__dict__.setdefault(y1, y2) \
    if y1 not in x.__dict__ else None, y.items()) else None


pont 3
 ここが今回のポイント sql のテーブル一覧を for で回した後、取得したテーブル情報 t をもとに、動的に生成したクラスを、動的に生成したグローバル定義に登録している。何を言っているか非常にわかりづらいが、 t.name が "PACAKGE" だった場合、

globals()[t.name]

とすると、PACKAGEはプログラム中どこでも使えるようになるため、その後はいきなり文中で PACKAGE と書いても未定義エラーにならない。じゃ、その PACKAGE には何が入るかというと、動的に生成されたクラスとなる。

type(str(t.name), (object,), classref)

は、 t.name のクラスを、 object のサブクラスとして定義して point 2で設定した初期化関数を追加して作成している。こうすることで、

PACKAGE( bar=1, foo=2 )

とすると、PACKAGEクラスのオブジェクトを生成できるようになる。
このままでは何もできないので、

mapper(globals()[t.name], Table(t.name, metadata, autoload=True))

とすることで、sql の実際のテーブルと関連付けを行っている


pont 4
ここまでが下準備で、あとは使ってみるだけである。テーブル名でクラスは定義されているため、test.sq lを見ながらコーディングをする。、 PACAKGEというクラスでデータを生成しては挿入した後に、検索をして、最後に挿入した一つを表示するようにしている。


まとめ
  このように、python は使えば使うほど手を抜く事ができる。本旨は point 3 だが、 point 2 でも一般的な python を使う上でのテクニックも紹介した。あえて触れていないが exception で何もしていないため、いろいろ危険である。分からない場合は使わないことをお勧めする。実際の私の環境では課題が起きにくいようにしているが、更に複雑で説明のしようがない。説明優先で簡単に書いているのでかんべんしてもらいたい

0 件のコメント:

コメントを投稿