LoginSignup
32
19

More than 5 years have passed since last update.

flaskでglobalに保持することを前提としたひどいライブラリを騙し騙し使う方法

Posted at

はじめに

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
32
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
19