# Flask のコンテキストってなに?

WARNING

書きかけです。

# はじめに

個人的に思う Flask のポイントは  グローバル変数  です。 具体的には flask.request, flask.current_app が該当します。 よく Flask の公式ドキュメントでは、コンテキスト context と表現されています。

Flask はグローバル変数を多用します。 そのため、どことどこが繋がってるか、若干わかりにくくなってしまいます。 しかし、反面、とても簡単にコードを書けるような設計になっています。

この記事では Flask の起動方法からはじめて、 最終的にコンテキストへと理解を繋げていきます。

# ◯ 参考文献

公式ドキュメントの「クイックスタート」と「チュートリアル」が、コンパクトにとてもよくまとまっています。

また、上記の動画は Flask の作者 Armin Ronacher 氏が Flask の設計背景などについて、 説明してくれているもので、とても勉強になります。

この記事と、この記事の前編にあたる Flask の view ってなに? は、 公式ドキュメントのチュートリアルを読んでいて、理解できなかったことをまとめています。

# ◯ 全体の流れ

まだ理解できていないのですが Flask の内側を理解する入り口として、 以下の3点があるのかなと思っています。

  1. 起動方法の違い
  2. プロセスの立ち上げ方
  3. WSGI
  4. session と g の違い
  5. コンテキスト
  6. スレッドセーフ

app.run(debug=False) としているのは、対話モード >>> でも動作するようにするため。 debug=True だと、ファイルの変更があった時に更新を読み込むため、そのファイルを探そうとするから、 こういうエラーが生じるとのこと。 相変わらず Stackoverflow が凄すぎる。

# 1. 複数の起動方法

Flask にはいくつか起動方法があります。 それが混乱を招いています。 基本的にサンルコード以外では app.run() は使わない方が良いかなと感じます。 なぜならパスの問題が起こるからです。

# 1.1. なんのサーバを起動しているのか?

app.run してる時のサーバは信頼できるのか?

flask コマンドの中身が何かを探って見ましょう。 which コマンドを使うと実際にどこが呼び出されているかわかります。

$ which flask
/Users/user/tutorial/venv/bin/flask
$

/Users/user/tutorial/venv/bin/flask が呼び出されているようです。 この中身はなんでしょうか?cat コマンドで中身を表示して見ましょう。 Python のファイルの様です。

$ cat /Users/user/tutorial/venv/bin/flask
#!/Users/sho/pythonms/labo/tutorial/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys

from flask.cli import main

if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
    sys.exit(main())
(venv) iMac:tutorial sho$ 
$ which flask

もっと深掘りできそうですが、今回はここまでにしておきます。

# 1.2. 補足 パス

どうやって flask コマンドと /Users/user/tutorial/venv/bin/flask を対応づけているのでしょうか? それはパスです。 パスで列挙されたディレクトリの中を探索してコマンドと同じ名前のファイル名が無いかを探しています。

$ echo $PATH | sed $'s/:/\\\n/g'
/Users/user/tutorial/venv/bin
/usr/local/bin
/usr/bin
/bin
/usr/sbin
/sbin
/opt/X11/bin
/opt/local/bin
/opt/local/sbin
/Users/user/bin
$ 

古いページなのでギョッとするかもしれませんが、この辺が参考になるかなと。

# 1.3. 補足 シバン

ファイルを実行しようにもなにで実行すればいいのかわかりません。 そのため、行頭に #! から始まるそのファイルを実行するための インタープリタを指定します。

#!/Users/user/tutorial/venv/bin/python3

この行頭の文字を シバン と呼ぶらしいです。

# 2. HTTP リクエストから Python

# 2.1. CGI

# 2.2. Apache - mod_wsgi

全然関係ないけど...

# 2.3. gunicorn, uwsgi

# 2.4. ngnix

# 3. WSGI

1つ目の WSGI の理解には、以下の動画がとても役に立ちます。

Flask そっくりなウェブアプリケーションを組んでくれています。 本当に、この動画を見るか見ないかで、見えてくる世界がかなり変わってきます。

おそらく公演時間の関係ですこし速口で話されていてびっくりするかもしれません。 でも腰を据えて見て欲しいです。これよりわかりやすくて良いものを僕は作れません。

この動画がなかったら一生 WSGI にたどり着くことができませんでした。 Flask を使われる方にとっては 100 億円の価値がある動画です。

# 4. current_app と request

# 4.1. flask.current_app

init_app というメソッドを見かけますが、 これはアプリケーションコンテクストというグローバル変数、状態を初期化しています。

db = SQLAlchemy()
        db.init_app(app)

これ、本当に自分も最初は何しているのかチンプンカンプンでした。

teratail にも似た様な質問が上がっています。

これの考え方としては app というグローバル変数に、 db が使うコンテクストを設定しています。 自分も最初これがなにを意図しているのか、わかりませんでした。

また Flask は、1度に複数のアプリケーションを持つことができます。 そのため g ではなく app に設定しているということなのでしょうか。

#
# 本物
#
from flask import Flask
app = Flask(__name__)

@app.route('/')
def index():
    return 'Hello World!'
#
# 偽物
#
from flask import route

@route('/')
def index():
    return 'Hello World!'

なぜ、このような設計になったかについては、こちらに書かれています。 結構、面白いです。

# 4.2. flask.request

コンテクストというと一般に文脈みたいな意味合いがあります。  ここではコンテクストはグローバル変数、みたいなものだと思って考えて下さい。 

なぜこのような言葉が生じたかというと Flask がグローバル変数 flask から request を受けるような設計になっているからです。

で、ここで着目していただきたいことは flask というグローバル変数から Request Context という「状態」を取得しています。

繰り返しになります、関数の引数として渡せばよかったのでは?と思いました。 関数の引数に request をバトン渡しするコードを書くのが嫌だったらしいです。 公式ドキュメントのどこかに書かれていた気がするのですが、失念しました...

import flask

@app.route('/')
def index():
    print(flask.request)
    return 'Hello, world!'
import flask

@app.route('/')
def book(request):  # <--- 引数で渡せばよかったんじゃないの?
    print(request)
    return 'Hello, world!'

上で見てきた引数付き url_for 関数のサンプルコードをもう一度持ってきます。 with 文を使っています。with 文を使ってそういうコンテクスト、文脈を設定しているということです。

#
# 対話モード >>> に
# コピペで実行できます。
#
import flask
app = flask.Flask(__name__)

# 1)
@app.route('/view0/')
def view0():
    return ''

#
# with 文を使って
# flask.request にリクエストを代入している。
#
with app.test_request_context('/view0/?arg0=hello&arg1=nihao'):
    print(flask.request.args['arg0'])
    print(flask.request.args['arg1'])
    print(flask.url_for(
        'view0', arg0='hello', arg1='nihao',))

# 5. session と g

# 5.2. flask.g

Flask には g があり、その配下に Application Context があり その配下にRequest Context という2つの Context があります。

通常 Python のグローバル変数というとモジュールの中だけですが、 g は Flask application 内のグローバル変数です。

Application Context は Application のグローバル変数を保持しています。 Request Context は Request のグローバル変数を保持しています。

この Context というのは、日本語で言えば「状態」になります。 例えば、Request Context を参照するには以下の様になります。

# 6. コンテキスト

コンテクストとはグローバル変数です。 Flask はグローバル変数を多用します。 これが理解へのポイントです。

Python にはスコープがあります。 例えば built-in, global, local という三段階層です。 その辺りの話はここで見ていきました。

これと同じようにして Flask にもスコープではないのですが、 名前空間がネストしています。 flask.g, flask.current_app, flask.request です。

そして、このコンテクスト, context は Flask の引っかかりどころです。 これらはコンテクストに関する疑問です。

# 6.1. コンテクストをどこで設定しているか

で、ここで問題になるのが、どうやってスレッドセーフを実現しているか、ということです。 グローバル変数に同時を同時に複数のスレッドが参照すると、 グローバル変数の中のオブジェクトがめちゃくちゃになってしまうことがあります。

$ # 4つのスレッドを起動する。
$ gunicorn flaskr:app --workers 4

1日数中アクセス程度では意識することがない。

--workers は「プロセス」の個数を指定しているらしいです。 基本的にプロセス間は、共有されないけど、スレッドだとグローバル変数が共有されてしまう。

マルチプロセスはメモリ空間が別ですので、共有はできません。 Multiprocessing内でグローバル変数は参照できないっぽい - Qiita

flask の設計はもれなくお亡くなりになる。

僕は並列化について一切知りません。

どの様にグローバル変数が、 めちゃくちゃになるかは Effective Python Item 38 に書かれていました。 いや Effective Python の内容に触れなくてもかけるか... もっと簡単な内容だ...

このあたりの話については Effective Python の5章「平行性と並列性」の Item 36 ~ Item 39 に目を通しておいた方がやりやすい、あるいはこの知識がないといくらか厳しい気がします。

より良い書籍もあるかもしれませんが、僕はこれしか知らず、勉強不足でまだ他の書籍に目を通せていません。

そのためグローバル変数として定義されたアプリケーションコンテクストや リクエストコンテクストは順番に参照される様になっていないといけません。 どこで、それを保証しているのか気になったので調べて見ました。

この方の動画を買ってはいないのですが、 一瞬チラ見したときに並列化について取り扱っていて ちゃんと図示して説明してくれていたので、 良い教材なのでは.. と思うのですが、 調査中です。

# 6.2. スレッドセーフを担保している箇所

これは WSGI の API です。この __call__ メソッドを gunicorn などのウェブサーバは呼び出します。

class Flask(_PackageBoundObject):

    ...

    def __call__(self, environ, start_response):
        """The WSGI server calls the Flask application object as the
        WSGI application. This calls :meth:`wsgi_app` which can be
        wrapped to applying middleware."""
        return self.wsgi_app(environ, start_response)

しかし __call__ メソッドは実質的に以下のメソッドをサイド呼び出しているだけです。 そしてここでスレッドセーフを実現しています。

class Flask(_PackageBoundObject):

    ...

    def wsgi_app(self, environ, start_response):
        ctx = self.request_context(environ)
        error = None
        try:
            try:
                ctx.push()  # <--- push して
                response = self.full_dispatch_request()
            except Exception as e:
                error = e
                response = self.handle_exception(e)
            except:
                error = sys.exc_info()[1]
                raise
            return response(environ, start_response)
        finally:
            if self.should_ignore_error(error):
                error = None
            ctx.auto_pop(error)  # <--- pop している

push してから auto_pop するまでの間、スレッドセーフが保たれます。 なぜかはわかりませんし、おそらくそうなのだろう程度で確証を得られていません。 push して pop するだけでスレッドセーフを実現しているだろうと推測しているのは、Effective Python を読んだからです。 これを読んでいなかったら気づけませんでした。 最初は with 文でやってるだろうと思っていたからです。

class Flask(_PackageBoundObject):

    ...

    def wsgi_app(self, environ, start_response):
        ctx = self.request_context(environ)

        ...

                ctx.push()  # <--- push して

            ...

            ctx.auto_pop(error)  # <--- pop している

self.request_context(environ) は RequestContext を返します。 では ApplicationContext はどこで push されているのでしょうか?

# 6.3. AppContext はどこで?

ここです。ctx.push() の中で同時に push されていました。

class RequestContext(object):

    ...

    def push(self):

        ...

        app_ctx = _app_ctx_stack.top
        if app_ctx is None or app_ctx.app != self.app:
            app_ctx = self.app.app_context()
            app_ctx.push()  # <--- ここで AppContext を push しています。
            self._implicit_app_ctx_stack.append(app_ctx)
        else:
            self._implicit_app_ctx_stack.append(None)

        if hasattr(sys, 'exc_clear'):
            sys.exc_clear()

        _request_ctx_stack.push(self)  # <--- RequestContext はここで push しています。

# 6.4. さらに深掘りすると...

RequestContext, AppContext を深掘りすると Context Locals にたどり着きます。

Context Locals をさらに掘り下げると gevent にたどり着きます。

gevent をさらに掘り下げると greenlet にたどり着きます。

とりあえずコードを辿っただけで中は一切見ていません。 とりあえず、いまは Flask という範囲の中で、どこでコンテクストをプッシュしてポップしているかだけ知りたかったので。

# 6.5. 思ったこと

個人的な感想ですが ctx.push() で ApplicationContext も同時に push されているとは思わなかった。 基本的に Flask はグローバル変数をガシガシ使っていくので、ちょっと辛い時がある。個人的にですが。

# Flask の設計
ctx.push()

# 個人的にはこうして欲しかった...
request_stack.push(request_context)
application_stack.push(application_context)

# 6.6. どうやって場所を特定したか

pdb の breakpoint を使いました。

# (1) breakpoint を設定する。

Python 3.7 から組み込み関数 breakpoint が新たに追加されました。 これを利用します。 適当な関数の中で breakpoint を呼び出します。

# sample.py
from flask import Flask
app = Flask(__name__)

@app.route('/')
def index():
    breakpoint()  # <--- 1行追加する。
    return ''

if __name__ == '__main__':
    app.run()

# (2) Flask を起動する。

python3 sample.py

# (3) ページにアクセスする。

breakpoint に達すると pdb が起動します。 where を打ち込みます。

(pdb)

# (4) where を打ち込みます。

(pdb) where

where スタックの底にある最も新しいフレームと一緒にスタックトレースをプリントします。 矢印はカレントフレームを指し、それがほとんどのコマンドのコンテキストを決定します。

するとスタックトレースが表示されます。スタックトレースというのは、関数が呼び出された順番が表示されたものです。 この中を1つ1つファイルを開いて確認していきました。括弧の中の数字は行番号です (885) なら 885 行目になります。

(Pdb) where
  /usr/local/Cellar/python/3.7.2_2/Frameworks/Python.framework/Versions/3.7/lib/python3.7/threading.py(885)_bootstrap()
-> self._bootstrap_inner()
  /usr/local/Cellar/python/3.7.2_2/Frameworks/Python.framework/Versions/3.7/lib/python3.7/threading.py(917)_bootstrap_inner()
-> self.run()
  /usr/local/Cellar/python/3.7.2_2/Frameworks/Python.framework/Versions/3.7/lib/python3.7/threading.py(865)run()
-> self._target(*self._args, **self._kwargs)
  /usr/local/Cellar/python/3.7.2_2/Frameworks/Python.framework/Versions/3.7/lib/python3.7/socketserver.py(651)process_request_thread()
-> self.finish_request(request, client_address)
  /usr/local/Cellar/python/3.7.2_2/Frameworks/Python.framework/Versions/3.7/lib/python3.7/socketserver.py(360)finish_request()
-> self.RequestHandlerClass(request, client_address, self)
  /usr/local/Cellar/python/3.7.2_2/Frameworks/Python.framework/Versions/3.7/lib/python3.7/socketserver.py(721)__init__()
-> self.handle()
  /Users/sho/.virtualenvs/inside-flask/lib/python3.7/site-packages/werkzeug/serving.py(325)handle()
-> rv = BaseHTTPRequestHandler.handle(self)
...

Flask は、グローバル変数 flask から request を取り出せるようにするために Thread Local という機能を導入しました。

いまは Thread Local という言葉は、無視してください。 Flask の公式ドキュメントに以下のような記述があります。

Thread Locals - Design Decisions in Flask

(Flask が採用した) Thread Local という仕組みは、 スレッドを使ったサーバでは問題が起こるし、 大きなアプリケーションを維持するのは難しくなります。
They cause troubles for servers that are not based on the concept of threads and make large applications harder to maintain.

しかしながら Flask は大きなアプリや非同期サーバのために設計されてはいません。 Flask は素早く簡単に一般的なウェブアプリを組むことを目標にしています。
However Flask is just not designed for large applications or asynchronous servers. Flask wants to make it quick and easy to write a traditional web application.

# 7. スレッドセーフ

別に大したことでは無いのですが、引っかかりどころです。 これら3つの詳細については、次のページで見ていきます。

Context Locals - Flask

スレッドによって制御されているコンテクストを想像してください。 リクエストが来てサーバがスレッドを生成すると決めました (もしくは、別の)。 Imagine the context being the handling thread. A request comes in and the web server decides to spawn a new thread (or something else, the underlying object is capable of dealing with concurrency systems other than threads).

スレッドローカルデータ - Python 標準ライブラリ

スレッドローカルデータは、その値がスレッド固有のデータです。スレッドローカルデータを管理するには、単に local (あるいはそのサブクラス) のインスタンスを作成して、その属性に値を設定してください:

mydata = threading.local()
mydata.x = 1

インスタンスの値はスレッドごとに違った値になります。

# おわりに

ここまで以下のように見てきました。

以上になります。ありがとうございました。