Flaskのカスタマイズについて

2012 Pythonアドベントカレンダー(Webフレームワーク) #python_adv の5日目の記事です。

明日は @altnight にお願いします。

Flaskはオモチャじゃないよ

PyCharmなどのIDEがFlaskに対応を始めたり、Flask はそろそろ Django に続く Python の Web フレームワーク No.2 と名乗れそうなほど広まってきています。(その割にPython3対応遅いけど)

Flaskと言えばマイクロフレームワークHello World が簡単に書けるヤツで有名ですよね。

import flask
app = flask.Flask(__name__)
@app.route('/')
def index():
    return "Hello, World."
app.run(debug=True)

これを見るとオモチャっぽく見えるんですが、Flaskは1つのPythonインタプリタに複数のアプリを載せたり、1つのアプリの複数のインスタンスを載せたりできるしっかりした土台を持っていて、Blueprintを使ったモジュール化にも対応していて、大規模アプリにも対応できる骨組みを持っています。

フルスタック型フレームワークと比べるとORマッパーやフォームライブラリやAJAXサポートライブラリなどのライブラリが組み込まれていないという点がマイクロなのですが、むしろそういったライブラリを自分で選びたい人はフルスタック型フレームワークを改造するよりもFlaskを拡張していくほうが簡単なケースが多いと思います。

今日はアプリごとにどうやって Flask をカスタマイズしていくかについてざっくり解説します。 (Flask-SQLAlchemy みたいにモジュール化したFlask拡張を作るのはまた今度の機会にします)

カスタマイズのパターン

Flaskクラスを継承してカスタマイズする方法と、Flaskインスタンスのメソッドを使う方法があるのですが、大抵の場合後者で事足りると思います。

カスタマイズ用の関数はたくさん用意されていますが、拡張する方法のパターンはそれほど多くないので、3つのパターンを紹介します。

appの属性を直接修正する

例えばヘッダー部分で使う変数など、アプリケーション全体でデフォルトで用意しておけば render_response で毎回変数を設定する必要がなくて便利です。 Flask は create_jinja_environment をオーバーライドするという拡張方法を用意しているのですが、この程度なら直接追加してしまったほうが楽です。

import random
app.jinja_env.globals['random'] = random.randint

ただし、 jinja_env は初めてアクセスされる時に create_jinja_environment を使って生成されるので、そのタイミングが早まってしまうことには注意が必要です。 殆どの場合は問題無いと思いますが、必要なら後述するデコレーター形式の拡張方法を使って

@app.before_first_request
def extend_jinja_env():
    app.jinja_env.globals['random'] = random.randint

のように最初のリクエストが来るまで遅延させたり、普通に create_jinja_environment をオーバーライドすると良いでしょう。

なお、jinja環境はアプリケーションに1つなので、 Blueprint 単位での変数の追加はできません。 その場合は blueprint を作っているモジュールで render 関数を書きましょう。

def render(tmpl, **kw):
    kw['rnd'] = random.randrange(1000)
    return flask.render_response(tmpl, **kw)

appの属性を直接修正する方法を使うケースは、例えば session_interface を置き換えて自前のセッションハンドラーを利用するなどがあります。

デコレーターを使った拡張

日本向けアプリなんだけどデータベースには時間をUTCで格納している場合など、Timezoneが+0なdatetimeをテンプレート内で日本時間として表示したい場合、こんな感じでフィルターを追加できます。

@app.template_filter()
def jptime(dt, format='%Y-%m-%d %H:%M:%S'):
    u"""utcの時間を日本時間で指定されたフォーマットで文字列化する."""
    local = dt + datetime.timedelta(hours=9)
    return local.strftime(format)

使うときはこんな感じ。

最終更新日時: {{ post.last_modified | jptime('%m/%d %H:%M') }}

@app.template_filter がただのデコレータじゃなくて関数形式のデコレーターになっていますが、この引数に name="JT" とか書くと関数名とテンプレート内でのフィルター名を別にすることができます。

デコレーターを使った拡張は他にもたくさんあります。また、その殆どが Blueprint でも利用可能です。 例えば、ある Blueprint では全リクエストに認証が必要だったとします。

from myapp.auth import auth
bp = flask.Blueprint('admin', __name__)

@bp.route('/')
@auth.required
def index():
    #…

@bp.route('/profile')
@auth.required
def profile():
    #…

before_request で関数を登録すると、その関数は view 関数呼び出し前に呼ばれます。 さらに、この関数がレスポンスを返すと view 関数の呼び出しがスキップされるので、view関数用の @auth.required デコレータを使うことができます。

bp = flask.Blueprint('admin', __name__)

@bp.before_request
@auth.required
def before_request():
    flask.g.user = auth.get_user()

#…

Flask の拡張用デコレーターには関数形式のものとただのデコレーターの2種類がありますが、使い方を間違っても確実に例外が起こってスタックトレース見れば一発でわかるので心配不要です。

appコンテキスト/requestコンテキストを使う

flask.current_app とか、flask.request のように、現在実行中のアプリケーションに関する情報、現在処理中のリクエストに関する情報を、それぞれコンテキストスタックと呼ばれるスレッドローカルなスタックを使って管理しています。 スタックになっているのは Flask アプリの中から他の Flask アプリを実行するなどができるようにするためで、あるアプリの拡張をしたい場合はそのスタックの一番上にあるコンテキストを編集します。

たとえば、リクエストごとにDBに接続して、終了時に切断したいとします。

この場合、requestコンテキストでもいいですが、HTTPリクエストがないスクリプトからでも手軽に使えるappコンテキストの方が適しています。

def _connect_db():
    return MySQLdb(**app.config['MYSQL_OPTS'])

def get_db():
    top = flask._app_context_stack.top
    if top is None:
        raise RuntimeError('working outside of application context')
    if not hasattr(top, 'db'):
        top.db = _connect_db()
    return top.db

@app.teardown_appcontext
def _close_db():
    top = flask._app_context_stack.top
    if top is None:
        raise RuntimeError('working outside of application context')
    if hasattr(top, 'db'):
        top.db.close()
        top.db = None

get_db() を毎回呼ぶ代わりにただのグローバル変数 db にしたい場合は、 LocalProxy を使います。

from werkzeug.local import LocalProxy
db = LocalProxy(get_db)

ただし、 LocalProxy にはオーバーヘッドがあるので、1リクエストの中で数百回とか頻繁に属性アクセスする場合は使わないほうが良いかもしれません。

実際には、直接コンテキストを変更しなくても、リクエストコンテキスト上の変数である flask.g を使ったほうが楽です。 (例: http://flask.pocoo.org/docs/patterns/sqlite3/ )

直接コンテキストを扱うのは、 Flask 拡張モジュールを作る場合など、固有のアプリケーションから独立して再利用性したい場合だけで十分だと思います。

flask.session を使わない

flask のセッションは Flask.session_interface を置き換えることでカスタマイズ可能です。

このカスタマイズは十分簡単なのですが、呼ばれるのが before_request の前なので、たとえばログイン必須なアプリでユーザーIDに紐付くセッションIDを生成したい場合にセッションオブジェクトの生成を認証前にしないといけなかったり、 Blueprint 単位で細かくセッションの扱いを制御したい場合などは面倒になります。 そこで、 flask.session を無効にする方法を紹介しておきます。

app.session_interface = flask.sessions.NullSessionInterface()

あとは独自のsessionを flask.g.session などに置いて、 before_request, teardown_request などを利用して動作を定義すれば、より自由にセッションを制御することができます。 例えば Flask で Beaker を使うには、 http://flask.pocoo.org/snippets/61/ を参考に、必要に応じてカスタマイズしてください。

このブログに乗せているコードは引用を除き CC0 1.0 で提供します。