Flaskの闇

Merry, Xmas.

Python advent calendar 2012 (#python_adv) 24日目の記事を、ミクパの再放送をBGMにお送りします。

今日は Flask のイケてないところとのつきあいかたを紹介します。

循環 import 問題

app.py 1ファイルだけの構成から成長してファイルを分け始めるときに突き当たるのが循環import問題です。 今まで1モジュールだった app.pymyapp/__init__.py にして、 view 関数を myapp/views.py の中で定義していきたいとします。

#myapp/__init__.py
from flask import Flask
app = Flask(__name__)

import myapp.views
#myapp/views.py
from myapp import app

@app.route('/')
def index():
    return 'hello'

view 関数に @app.route デコレータを使うには views.pyfrom myapp import app をしないといけないんだけど、 __init__.py の中から views.py を import しないと初期化が終わらないという相互再帰構造になっています。

Larger Applications のノートにあるとおり、 __init__.py の最後で views.py を import しているのがミソです。この import が app の定義より前にあると、 views.py は app を使えません。

これで一応問題ないのですが、この回避方法はあまり推奨できるものではありません。絶対 import (from myapp import app) を相対 import (from . import app) に書き換えるだけで、 http://stackoverflow.com/questions/8030264/relative-import-problems-in-python-3 にあるような問題にぶつかる危険があります。

できるだけモジュールを分離して循環 import が起こらないようにする、循環 import をする場合は、 モジュールを import するだけにしてそのモジュールの属性を使うのは import が完全に終わった後に実行される関数の中だけにするのがお勧めです。 import 中で初期化が終わってないモジュールを使うのはやめましょう。

Flaskでは、とりあえず __init__.pyapp を作らないのが良いと思います。 代わりに、 _app.py などを作ってそこで app を生成し、 __init__.pyfrom ._app import app; from . import views するだけにしてしまいます。 views.py でも from ._app import app できます。 こうすると _app.py は、 app オブジェクトを生成するだけで役目が終わって他のモジュールに依存しなくなるので、巡回 import は避けられます。

もしくは、 @app を直接使うのはやめて必ず Blueprint を使う方法もお勧めです。

#myapp/__init__.py
from flask import Flask

app = Flask(__name__)

from .views import bp
app.register_blueprint(bp)
#myapp/views.py
from flask import Blueprint

bp = Blueprint('top', __name__)

@bp.route('/')
def index():
    return 'hello'

view の中で app を使いたい時は、 flask.current_app というプロキシオブジェクトを経由することで、直接 app オブジェクトに依存することを避ける事ができます。

Flask拡張の品質問題

Flask Extension Development では、 Application Factory に対応したお行儀の良い Flask 拡張の作り方を紹介しています。 その拡張は次のようにして使うことができます。

db = SQLite()

# application factory は複数回呼ばれ得るので、 db は複数の app と協調して動かないといけない

def make_app():
    app = flask.Flask(__name__)
    app.config.from_pyfile('config.py')
    db.init_app(app)

# 現在実行中の app に依存して動く
    cur = db.connection.cursor()
    cur.execute(…)

また、そのドキュメントで明言はされていませんが、この SQLite は接続時に初めて設定を読むようになっているので、 make_app() を実行した後に app.config を書き換えても動くようになっています。

このように御行儀良く作られている Flask 拡張なら、ユニットテストでちょっと app の設定を変えて実行するなどしても問題が起こらず、使い勝手が良いです。

ところが、このドキュメントのサンプルコードにはバグがあります。

    def connect(self):
        return sqlite3.connect(self.app.config['SQLITE3_DATABASE'])

init_app() を利用された場合は self.app を保存しない (特定の app インスタンスに依存せず、 current_app を利用するようにする) と説明しているのにもかかわらず、 self.app を使っちゃっているので説明通りに動きません。

このバグは気づいてすでにバグ報告したのですが、今さらこんな基本的なバグが公式の拡張モジュールの作り方を解説に含まれていることから想像できる通り、かなりの Flask 拡張が「お行儀のいい」実装になっていません。公式サイトに載っている approved extension ですらお行儀の悪いものがあふれています。

なので、 Flask 拡張のお行儀の良さに頼るのをやめましょう。複数の app インスタンスを同時に使うのは論外ですし、「テストの時にちょっと設定をいじった app オブジェクトを作り直す」ためには Application Factory で拡張も含めて全部作りなおしてしまえば、もう何も怖くありません。

db = app = None

def make_app(conf):
    global app, db
    app = flask.Flask(__name__)
    app.config.update(conf)
    db = SQLite(app)

理想は理想であると見抜けない人には(Flaskを使うのは)難しい

こんな感じで、 Flask は公式サイトにある推奨通りの方法を使えば地雷を避けられるフレームワークではありません。 Flaskを使う場合は、Flask本体や利用する拡張モジュールのソースを自分で読みましょう。

問題にぶつかった時に自分でソースを読んで解決できるのであれば、Flaskは、使い始めるのが簡単でラフにカスタマイズすることができるとても便利な道具となってくれます。

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