今週はISUCON7予選ですね。ここのところ毎回ですが今回もISUCON関連のネタです。ISUCONのPython実装はFlaskで作成されているケースが多いですが、これをBottleに書き換えるとパフォーマンスが上がるケースがあります。しかし書き換えにはいくつか注意点があります。
なぜ、FlaskとBottleでパフォーマンスが違うのか
FlaskのほうがBottleに比べると高機能です。もちろんDjangoといったフルスタックフレームワークと比べればFlaskはシンプルですが、それでもBottleにくらべると同じ処理でもフレームワーク内部の処理量が結構違います。
ISUCONで出題されるレベルのアプリケーションであれば、Bottleの範囲に収まる事が多いためフレームワークのオーバヘッドの分、パフォーマンスが稼げる可能性があります。
書き換えるときの注意点
今回は@showwinさんが作成されたオリジナルのISUCONであるISOCON1を参考に見ていきます。
from flask import Flask, request, abort, session, render_template, redirect
このあたりがBottleでどうなるのかという点がポイントです。
app.route
Flaskでは
app = Flask(__name__, static_folder=str(static_folder), static_url_path='')
こんな感じでapp
を作り、
@app.route('/login', methods=['POST']) def post_login(): :
といった感じでルーティングを組み立てています。Bottleではこんな感じになります。
import bottle : app = bottle.default_app() : @app.post('/login') def post_login(): :
ここらへんは違和感ないですね。
request
リクエストオブジェクトについてはFlaskとBottleで大差はありません。
from flask import Flask, request,.... : def db(): if hasattr(request, 'db'): return request.db else:
from bottle import request : def db(): if hasattr(request, 'db'): return request.db else:
abort
エラーレスポンスを戻すときに使いますが、abort()
自体は差はありません。
from flask import ...., abort... def authenticated(): if not current_user(): abort(401)
from bottle import abort def authenticated(): if not current_user(): abort(401)
ただし、エラーページの扱いが違います。
@app.errorhandler(401) def authentication_error(error): return render_template('login.html', message='ログインに失敗しました'), 401
@app.error(401) def authentication_error(error): # bottleでは,401でタプルを戻すと死ぬ return render_template('login.html', message='ログインに失敗しました')
app.error()
デコレータになったり、returnでタプルを戻すと死ぬので注意です。
session
sessionはだいぶ違います。Flaskは標準でもっていますが、Bottleはプラグインが必要です。
from flask import ..., session,... : def current_user(): if 'user_id' in session: cur = db().cursor() cur.execute('SELECT * FROM users WHERE id = %s', (str(session['user_id']),)) return cur.fetchone() else: return None
from beaker.middleware import SessionMiddleware : # セッション管理の設定 session_opts = { 'session.type': 'file', 'session.data_dir': '/tmp/data', 'session.cookie_expires': True, 'session.auto': True } : def current_user(): session = bottle.request.environ.get('beaker.session') if 'user_id' in session: cur = db().cursor() cur.execute('SELECT * FROM users WHERE id = %s', (str(session['user_id']),)) return cur.fetchone() else: return None : # sessionの有効化 app = SessionMiddleware(app, session_opts)
beaker
モジュールを別途導入した上で、SessionMiddleware
を使う必要があります。またsession
を使うにはbottle.request.environ.get('beaker.session')
として取得しなければいけません。結構このあたりは手間ですね。また、session_optsの最適解がイマイチわかりません。
render_template
テンプレートをレンダリングする方法は異なりますが、揃えることは可能です。特にFlaskでJinja2を使っているのであれば、そのままBottleもJinja2を使えばいいだけです。
from flask import ..., render_template : @app.route('/') def get_index(): : : return render_template('index.html', products=products, current_user=current_user())
from bottle import TEMPLATE_PATH from bottle import jinja2_template as render_template import pathlib # テンプレート探索先に追加 TEMPLATE_PATH.append(pathlib.Path(__file__).resolve().parent / 'templates') @app.route('/') def get_index(): : : return render_template('index.html', products=products, current_user=current_user())
TEMPLATE_PATH
の調整は必要ですが、bottle.jinja2_template
をrender_template
にしてしまえば差し替えるのは容易です。
ルーティングのフィルタ
地味に差があって混乱します。
# Flask では <データ型:変数名> @app.route('/users/<int:user_id>') def get_mypage(user_id): cur = db().cursor() :
# Bottleでは<変数名:データ型> @app.get('/users/<user_id:int>') def get_mypage(user_id): cur = db().cursor() :
逆になっているので、差し替えるときはそれぞれ書き換えなければいけません。
リクエストのフック
DBとの接続を閉じる等、リクエスト終了毎に何かを行うような場合のHOOKの書き方が異なります。
@app.teardown_request def close_db(exception=None): if hasattr(request, 'db'): request.db.close()
@bottle.hook('after_request') def close_db(exception=None): if hasattr(request, 'db'): request.db.close()
@app.teardown_request
ではなく@bottle.hook('after_request')
としてデコレータを付与します。
まとめ
なかなか機会が絞られる知識のTips集みたいになってしまいましたが、FlaskをBottleに書き換えることができれば多少はパフォーマンスが稼げますので頭の片隅においておくといいのではないでしょうか。