Flask-Injector を試してみた

Python というか Flask でも DI コンテナ欲しいなーと思う事がある。
去年書いていたコードでは最初アップロードしたファイルの保存を Flask が動いているサーバのファイルシステムに保存していたが、後から RiakCS に保存するという仕様変更を行った。


こんなイメージ。
models/storage.py

# -*- coding: utf-8 -*-
# インターフェイスのつもり
class Base(object):
    def save(self):
        raise NotImplementedError()


class FileStorage(Base):
    def save(self, file, dest):
        # ローカルに保存
        with open(dest, 'r') as f:
            f.write(file)


class RiakCSStorage(Base):
    def save(self, file, dest):
        # boto を使ってファイルを保存


class Storage(object):
    def __init__(self, storage=FileStorage):
        self.storage = storage()

    def save(self, file, dest):
        self.storage.save(file, dest)

Flask の view

from models.storage import Storage, FileStorage

@app.route('/upload')
def upload():
    storage = Storage(FileStorage)
    storage.save(request.file, '/tmp/storage')

作っている最中に後々 RiakCS を使うだろうなーと思っていたので、DI する感じで作っていた。

from models.storage import Storage, RiakCSStorage

@app.route('/upload')
def upload():
    storage = Storage(RiakCSStorage) # FileStorage から RiakCSStorage に
    storage.save(request.file, '/tmp/storage')

こんな感じで DI してたが、view の中でインスタンスを生成してるのが気持ち悪い。
コンテナ使いたい! ってなって Flask の view で依存を排除したい - Memo こんな方法を考えたりしたが、どうもしっくり来なかった。


年末に Injector という DI コンテナの実装があって、しかも Flask の拡張がある事を知った。
GitHub - alecthomas/flask_injector: Adds Injector support to Flask.
この Injector というのは Guice の影響を受けているよう。


とりあえず試してみた。
app.py

# -*- coding: utf-8 -*-
from flask import Flask
from flask.ext.injector import init_app, post_init_app
from binds import configure
from views.index import app as index_view


def create_app():
    app = Flask(__name__)
    app.register_blueprint(index_view)
    injector = init_app(app=app, modules=[configure])
    post_init_app(app, injector)
    
    return app

def main():
    app = create_app()
    app.debug = True
    app.run()

if __name__ == '__main__':
    main()

binds.py

# -*- coding: utf-8 -*-
from flask import Flask
from injector import inject, singleton, Key
from models.base import Base
from models.filestorage import FileStorage
from models.riakcs import RiakCSStorage

# inject してるのは configure 内で Flask のインスタンスを触る場合に利用する
@inject(app=Flask)
def configure(binder, app):    
    binder.bind(Base, to=FileStorage, scope=singleton)

models/base.py

# -*- coding: utf-8 -*-
class Base(object):
    def __init__(self):
        print 'base init'
    
    def save(self):
        raise NotImplementedError()

models/filestorage.py

# -*- coding: utf-8 -*-
from base import Base


class FileStorage(Base):
    def __init__(self):
        pass
        
    def save(self):
        return 'save to local fs'

models/riakcs.py

# -*- coding: utf-8 -*-
from base import Base

    
class RiakCSStorage(Base):
    def __init__(self):
        pass
        
    def save(self):
        return 'save to riakcs'

views/index.py

# -*- coding: utf-8 -*-
from injector import inject
from flask import Blueprint
from models.base import Base

app = Blueprint('index', __name__)


@app.route('/', strict_slashes=False, methods=['GET'])
@inject(service=Base)
def index(service):
    print service

    return service.save()

これで app.py を実行して、locahost:5000 にアクセスすると標準出力に以下のように表示される。

$ python app.py
 * Running on http://127.0.0.1:5000/
 * Restarting with reloader
<models.filestorage.FileStorage object at 0x10ebc0050>

引数の service 変数には FileStorage クラスのインスタンスが格納されている。
binds.py を FileStorage から RiakCSStorage に変更する。

@inject(app=Flask)
def configure(binder, app):    
    binder.bind(Base, to=RiakCSStorage , scope=singleton)

実行する。

$ python app.py
 * Running on http://127.0.0.1:5000/
 * Restarting with reloader
<models.riakcs.RiakCSStorage object at 0x10d538fd0>

今度は service は RiakCSStorage のインスタンスに代わり、めでたく view のコードを一切変更せずに model のインスタンスを差し替えられた。
binds の scope にはアプリケーション起動時にインスタンスを生成する singleton やリクエストごとに生成する request scope、スレッドごとに生成する local-thread scope などがある。
# scope 自体は自分で作る事もできる模様

まとめ

view の中でインスタンスを生成しているか binds の中で指定してるかの違いだけな気がしないでもないが、Flask-Injector 面白い*1


個人的に DI コンテナというと Seasar2 から入った口なので、XML ファイル(設定ファイル)でクラスを指定したりする方が運用を行う上では良いと思ってる。
実際に S2Robot を使ってクローラを作った際に Service クラスを差し替えたいと思った時に元コードを触らず、XML ファイルを変更するだけで自分の Service クラスに差し替えられてめっちゃ便利!! って体験があった。
そういう意味では Guice のスタイルの DI コンテナはどうせソースコードを変更するのなら、インスタンス生成してる所直接書き換えればええやんと思ってしまうのは、たぶん理解が浅いんだろうな。

*1:Flask-Injector も Guice の影響を受けてるのでソースコードを変更するが、binds.py は設定ファイルと思い込む事でお茶を濁す事にするw