はじめに
flaskで以下のようなglobalでの利用を前提としたライブラリがあったりします。
from flask import Flask
from something import Something
class settings:
MESSAGE = "hello from something"
app = Flask(__name__)
app.config.from_object(settings)
hmm = Something(app) # これ
@app.route("/")
def hello():
return hmm.hello() # ここ
if __name__ == "__main__":
app.run(port=4040)
中のコードを覗いてみると、appの代替をしてくれるわけでもなくappを引数に取るのはconfigの情報を取得したいからのようでした。本来はこのようなライブラリを使いたくはないのですが。使わなくてはいけない場合もあります。
実装自体は以下の様になっていました。
class Something(object):
def __init__(self, app=None):
self.init_app(app)
def init_app(self, app):
# configを見て何かする
self.message = app.config["MESSAGE"]
def hello(self): # 呼びたいメソッド
return self.message
appを直接globalに置きたくない
appを直接globalに置きたくない。例えばviewの定義もblueprintなどを使うのが普通だと思います。ところが以下のように書き換えると問題が生じます。
views.py
from flask import Blueprint
b = Blueprint("hello", __name__)
@b.route("/")
def hello():
return hmm.hello() # これ呼びたい
app.py
def make_app():
class settings:
MESSAGE = "hello from something"
app = Flask(__name__)
app.config.from_object(settings)
hmm = Something(app) # これに触る方法がない
app.register_blueprint(b)
return app
if __name__ == "__main__":
app = make_app()
app.run(port=4040)
appの生成を関数で包んでしまうと利用したいhmmにアクセスする方法がなくなります。かと言って、make_appの戻り値でhmmも返すようにしてとやってしまうとglobal変数にしたことと結局変わらない状態になってしまい本末転倒です。
thread local object
thread local objectを使うというのがflaskの文化らしいです。例えばrequestオブジェクトなどはthread localなものです。これに倣った感じでやるという方法が良いかもしれません。ちなみにthread localにしたい場合には以下の様にすれば良いです。current_appとgもthread localです。
from flask import g, current_app
from werkzeug.local import LocalProxy
def find_hmm():
print("hoi")
if not hasattr(g, "hmm"):
print("hai")
g.hmm = Something(current_app)
return g.hmm
hmm = LocalProxy(find_hmm)
1 requestの間では共有できます。当然ですが新規のrequestが来るたびに再生成されます。それが嫌な場合もあるかもしれません。
http://localhost:4040/ に2回リクエストを投げた場合には以下の様になります。
hoi
hai
hoi
hai
thread local objectと似たようなinterface
本当に1個のsingletonを持ちたい場合もあるかもしれません。globalなproxyを公開するというのがflaskの文化らしいのでそれに倣って似たようなインターフェイスのオブジェクトを作りましょう。
class LazyOnceEvalObject(object):
def __init__(self, fn):
self._fn = fn
self.proxy = None
def __getattr__(self, name):
if self.proxy is None:
self.proxy = self._fn()
print("hai")
return getattr(self.proxy, name)
def find_hmm():
print("hoi")
return Something(current_app)
hmm = LazyOnceEvalObject(find_hmm)
初回のrequest時にだけ find_hmm()
によりhmmが生成されます。
http://localhost:4040/ に2回リクエストを投げた場合には以下の様になります。
hoi
hai
hai
初期化処理に時間が掛かる場合
初期化処理に時間がかかる場合があります。(初回だけとは言え)リクエスト時にhmmのproxyが初期化されるというのでは負担がおおきすぎる場合があります。そのような場合は無理やりapplication contextを作って設定してあげると良いかもしれません。
def make_app():
class settings:
MESSAGE = "hello from something"
app = Flask(__name__)
app.config.from_object(settings)
app.register_blueprint(b)
with app.app_context():
hmm.hello()
return app
appを渡せるようにあれこれと頑張るよりはcurrent_appで取り出せるように明示的にcontextを作ってしまうのが楽かもしれません。
おまけ的な話
contextを新たに作ってしまうというのはテストのときにも役に立つかもしれません。例えば以下のようなコードを実行すると、f0の中でg.fooに値を入れたあと、app_context()で新たにcontextを作った後にf0を呼んでいるので2度目のf1ではNoneになります。
def f0():
g.foo = "foo"
print("f0 before with")
f1()
with current_app.app_context():
print("f0 after with")
f1()
def f1():
print(getattr(g, "foo", None))
with app.app_context():
f0()
結果
f0 before with
foo
f0 after with
None