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

## はじめに

flaskで以下のようなglobalでの利用を前提としたライブラリがあったりします。

```python
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の情報を取得したいからのようでした。本来はこのようなライブラリを使いたくはないのですが。使わなくてはいけない場合もあります。

実装自体は以下の様になっていました。

```python
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

```python
from flask import Blueprint
b = Blueprint("hello", __name__)


@b.route("/")
def hello():
    return hmm.hello()  # これ呼びたい
```

app.py

```python
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です。

```python
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の文化らしいのでそれに倣って似たようなインターフェイスのオブジェクトを作りましょう。

```python
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を作って設定してあげると良いかもしれません。

```python
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になります。

```python
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
```