Last Updated: 2/6/2024, 5:44:57 AM

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

WARNING

書きかけです。

グローバル変数を
管理する箱です。
コンテキスト 用途
アプリケーション
コンテキスト
current_app, g を保存する箱
リクエスト
コンテキスト
request, session を保存する箱
コンテキスト 変数 用途
アプリケーション
コンテキスト
current_app 省略
アプリケーション
コンテキスト
g 主にリクエストを跨いで
使用するオブジェクトを保存する。
しかし、リクエストを跨いで
オブジェクトは保存  されない。 
リクエスト
コンテキスト
request 省略
リクエスト
コンテキスト
session 主にリクエストを跨いで
使用するオブジェクトを保存する。
リクエストを跨いで
オブジェクトが保存  される。 
 ただし、シリアライズ可能なもののみ。 

# はじめに

個人的に思う Flask のポイントは  グローバル変数  です。 具体的には request, current_app, session, g です。

コンテキスト context は、これらの4つのグローバル変数を保存する箱です。 コンテキスト context には2種類あります。 Applicatoin Context と Request Context です。

そして、コンテキスト, context は Flask の引っかかりどころでもあります。 以下はコンテクストに関する疑問です。

このコンテキストという言葉は、 おそらく情報工学におけるコンテキストから 引っ張ってきたものだと思われます。

この記事では Flask の起動方法からはじめて、 最終的にコンテキストへと理解を繋げていきます。 結論だけ読めるようになっています。 下記のサイト内リンクから飛んでください。

Hello, world!

# ◯ 参考文献

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

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

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

# 1. 2 つの起動方法

Flask には2つの起動方法があります。 それで混乱しました笑 ここで以下のような hello.py があったとします。

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, World!'

# 1.1. app.run()

次のように起動することがあります。

$ python hello.py

app.run() として描く方法は Qiita など個人ブログにサンプルコードを貼り付ける場合は、とても便利です。 しかし、それ以外ではあまり使わない方が良いかなと感じます。

なぜなら、うまく説明できないのですが、 複数のファイルに分割した時に import 文のパスを書き直さないといけなくなるからです。

# 1.2. flask run コマンド

次のように起動することがあります。

$ export FLASK_APP=hello.py
$ flask run

# ◯ なんで2つの起動方法があるの?

app.run() メソッドがあるのに、なぜ flask run メソッドがあるのでしょうか?

上記で紹介した動画によると1つ目の理由は app.run() の場合、 SyntaxError が起こると停止してしまうからとのことでした。

また2つ目の理由は、とある会社で app.run(debug=True) を運用サーバで投げ込んで、 ハックされて、データベースの中身を全部抜かれたことがあったそうです。 そのため環境変数で変えられれば、この問題を解決できます。

どうやってデータベースから引っこ抜いたんだろうと思って調べて見たのですが、 どうも Werkzeug のデバッガがリモートからでもコードが実行できるようになるそうです。

当該事件を説明したと思われる記事。

# ◯ 中で動かしているサーバの違いは?

ただ、内部的には app.run() でも flask 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/user/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 user$ 
$ which flask

もっと深掘りできそうですが、今回はここまでにしておきます。 ここでお伝えしたかったことは flask コマンドは Python のスクリプトだということです。

# ◯ パス

どうやって 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
$ 

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

# ◯ シバン

ファイルを実行しようにもなにで実行すればいいのかわかりません。 そのため、行頭に #! から始まるそのファイルを実行するための インタープリタを指定します。 この行頭の文字を シバン (opens new window) と呼ぶらしいです。

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

# 2. 複数の構成

負荷の関係で、いくつかの構成があります。

普段使っている Python は1プロセス1スレッドで動いています。 プロセスは、召喚に必要なエネルギーが大きい召喚獣です。 スレッドは、召喚に必要なエネルギーが小さい召喚獣です。

呼ばれる度に召喚獣を呼び出してたら重たいので、常に起動させておきます...(よう修正)

こちらのツイートがとても勉強になります。

「プロセス」、「スレッド」をキーワードにして調べて見てください。 自分は、以下の本で色々と勉強させていただきました。

またこちらの動画でも勉強させていただきました。 Python におけるマルチプロセスとマルチスレッドの動作の違いを説明してくれています。

プロセスについては PyCon の動画も参考になります。

# 2.1. 試験用サーバ

# 2.2. gunicorn, uwsgi と Nginx

試験用のサーバは1台だったのですが、2台

# 2.3. mod-wsgi と Apache

全然関係ないけど...

# 2.4. CGI

# 3. WSGI

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

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

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

この動画がなかったら一生 WSGI にたどり着くことができませんでした。 Flask を使われる方には、大変オススメしたいです。

# 4. 4 つのグローバル変数

# 4.1. request

他の3つのグローバル変数が書き込めるのに対して、 request は読み込み専用です。 これはよく触られていると思うので、説明を割愛します。

# 4.2. session

session は、クッキーを使って実装されています。 サーバが HTTP ヘッダに値を書き込みレスポンスを返すと、 クライアントは、次回のリクエスト時にも同じ値を値を書き込んで返すように定められています。

Set-Cookie: key=value;

HTTP Cookie - MDN web docs (opens new window)
HTTP Cookie (ウェブ Cookie、ブラウザー Cookie) は、サーバーがユーザーのウェブブラウザーに送信する小さなデータであり、ブラウザーに保存されて次のリクエストと共に同じサーバーへ返送されます。

「クッキーは危ないからセッションを使おう」みたいな記事を見かけました。 「クッキーとセッションって違うものなのかな?」って思ったのですが。 あまり詳しいことはわかっていないのですが、少なくとも Flask に限って言えば、 クッキーでセッションを実装しています。

なぜクッキーが危ないかというとデフォルトだとクッキーの値を JavaScript で取り出せてしまうので、 XSS などを食らうとセッションハイジャックを食らってしまうそうです。

HttpOnly フラグを立てることで JavaScript からは参照できなくなるそうです。

Set-Cookie: key=value; HttpOnly

HTTP Cookie - MDN web docs (opens new window)
クロスサイトスクリプティング (XSS) 攻撃を防ぐため、HttpOnly の Cookie は、JavaScript の Document.cookie API からアクセスすることができません。これらはサーバーにのみ送られます。例えば、サーバー側セッションを維持するための Cookie は JavaScript で利用する必要がないので、HttpOnly フラグを設定するべきです。

session はクッキーを使ってリクエストを跨いでオブジェクトを保存してくれます。 クッキー、HTTP のヘッダに保存するので、シリアライズ可能なもののみです。

# 4.3. g

反面 g は、代わりにどんなオブジェクトも保存できます。 しかし、コンテキストが終了すると破棄されてしまいます。

そのためリクエストを跨いでオブジェクトは保存されません。 sessiong を組み合わせてオブジェクトを保存します。

session には user_id を保存します。 実際の User クラスのオブジェクトは user_id を取り出し、 データベースにクエリを掛けて取り出し g に保存するような書き方をします。

@bp.before_app_request
def load_logged_in_user():
    """If a user id is stored in the session, load the user object from
    the database into ``g.user``."""
    user_id = session.get("user_id")

    if user_id is None:
        g.user = None
    else:
        g.user = (
            get_db().execute("SELECT * FROM user WHERE id = ?", (user_id,)).fetchone()
        )

# 4.4. current_app

# current_app が存在する意味

なんで current_app が存在するのでしょうか?

現在のリクエストを処理するアプリケーションへのプロキシです。 これは import する必要なしにアプリケーションを参照したい場合、 またはアプリケーションを import できない場合に便利です、 import できない場合とは、例えば、アプリケーションファクトリパターンを使用していたり、 あるいは blueprint や拡張機能内部のコードで直接 app を import できない場合です。
A proxy to the application handling the current request. This is useful to access the application without needing to import it, or if it can’t be imported, such as when using the application factory pattern or in blueprints and extensions.
flask.current_app (opens new window)

また循環 import を避けるためのものの様です。

view の中で app を使いたい時は、 flask.current_app というプロキシオブジェクトを経由することで、 直接 app オブジェクトに依存することを避ける事ができます。
循環 import 問題 - methaneのブログ (opens new window)

# g とcurrent_app.config の使い分け

current_app.config というグローバル変数を保存する場所があります。 g とはどう使い分ければいいのでしょうか?

g はリクエストに固有のもの app.config はアプリケーション全体で使うものかなと個人的に思っています。 これがややこしかったです。

SQLAlchemy に限らず、Flask では init_app というメソッドを度々見かけます。

db = SQLAlchemy()
db.init_app(app)

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

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

コードを見れば大したことないのですが app.config に色々と設定しています。 意味合いとしては、Flask クラスの app というオブジェクトをグローバル変数に見立ています。 init_app はこのグローバル変数を初期化しています。

class SQLAlchemy(object):

    ...

    def init_app(self, app):
        """This callback can be used to initialize an application for the
        use with this database setup.  Never use a database in the context
        of an application not initialized that way or connections will
        leak.
        
        (だいぶ意訳)
        この関数を使えば、
        このデータベースのセットアップするために、アプリケーションを初期化することができます。
        アプリケーションコンテキストの中でデータベースを使わなかった場合、
        データベース接続が漏れてしまいます。
        """
        if (
            'SQLALCHEMY_DATABASE_URI' not in app.config and
            'SQLALCHEMY_BINDS' not in app.config
        ):
            warnings.warn(
                'Neither SQLALCHEMY_DATABASE_URI nor SQLALCHEMY_BINDS is set. '
                'Defaulting SQLALCHEMY_DATABASE_URI to "sqlite:///:memory:".'
            )

        app.config.setdefault('SQLALCHEMY_DATABASE_URI', 'sqlite:///:memory:')
        app.config.setdefault('SQLALCHEMY_BINDS', None)
        app.config.setdefault('SQLALCHEMY_NATIVE_UNICODE', None)
        app.config.setdefault('SQLALCHEMY_ECHO', False)
        app.config.setdefault('SQLALCHEMY_RECORD_QUERIES', None)
        app.config.setdefault('SQLALCHEMY_POOL_SIZE', None)
        app.config.setdefault('SQLALCHEMY_POOL_TIMEOUT', None)
        app.config.setdefault('SQLALCHEMY_POOL_RECYCLE', None)
        app.config.setdefault('SQLALCHEMY_MAX_OVERFLOW', None)
        app.config.setdefault('SQLALCHEMY_COMMIT_ON_TEARDOWN', False)
        track_modifications = app.config.setdefault(
            'SQLALCHEMY_TRACK_MODIFICATIONS', None
        )

        if track_modifications is None:
            warnings.warn(FSADeprecationWarning(
                'SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and '
                'will be disabled by default in the future.  Set it to True '
                'or False to suppress this warning.'
            ))

        app.extensions['sqlalchemy'] = _SQLAlchemyState(self)

        @app.teardown_appcontext
        def shutdown_session(response_or_exc):
            if app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN']:
                if response_or_exc is None:
                    self.session.commit()

            self.session.remove()
            return response_or_exc

これの考え方としてはアプリケーションコンテキストに db が使うグローバル変数を設定しています (正確にはアプリケーションコンテキストではなく app.config ですが)。 自分も最初これがなにを意図しているのか、わかりませんでした。

# 5. 2 つのコンテキスト

コンテキスト 用途
アプリケーション
コンテキスト
current_app, g を保存する箱
リクエスト
コンテキスト
request, session を保存する箱
コンテキスト 変数 用途
アプリケーション
コンテキスト
current_app 省略
アプリケーション
コンテキスト
g 主にリクエストを跨いで
使用するオブジェクトを保存する。
しかし、リクエストを跨いで
オブジェクトは保存  されない。 
リクエスト
コンテキスト
request 省略
リクエスト
コンテキスト
session 主にリクエストを跨いで
使用するオブジェクトを保存する。
リクエストを跨いで
オブジェクトが保存  される。 
 ただし、シリアライズ可能なもののみ。 

# 5.1. なんで2つのコンテキストがあるの?

アプリケーションコンテキストとリクエストコンテキストが分かれている理由は、リクエストコンテキストが要らない時があるからです。 例えば、環境構築時にデータベースを作成したりする操作にはリクエストコンテキストは不要です。

たとえば、リクエストごとにDBに接続して、終了時に切断したいとします。 この場合、requestコンテキストでもいいですが、HTTPリクエストがないスクリプトからでも手軽に使えるappコンテキストの方が適しています。
Flaskのカスタマイズについて - Qiita (opens new window)

# 5.2. コンテキストの生成から破棄まで

WSGI が呼び出されるとコンテキストが生成されます。 次にビュー関数が呼び出されます。 ビュー関数が終了し、返り値を返してくると、 コンテキストは破棄されます。

この場所は、意外と簡単に見つけられました。 WSGI のインターフェイスを見つけます。 おや? self.wsgi_app を呼び出しているだけです。

class Flask(_PackageBoundObject):

    ...

    def __call__(self, environ, start_response):

        ...

        return self.wsgi_app(environ, start_response)

ミドルウェアを実装する際に、直接 __call__ をラップされてしまうと 他の属性を参照できなくなります。 これを避けるために wsgi_app をラップすればいいようにしています(ここは説明が雑です)。

コンテキストの生成と破棄は、この wsgi_app 関数の中で見ることができます。

class class Flask(_PackageBoundObject):

    ...

    def wsgi_app(self, environ, start_response):

        ...
        
        # 1. コンテキストを生成(リクエストコンテキストです。)
        ctx = self.request_context(environ)
        error = None
        try:
            try:
                # 2. コンテキストを push
                ctx.push()
                # 3. ビュー関数の呼び出し
                response = self.full_dispatch_request()

            except Exception as e:
                ...
            except:  # noqa: B001
                ...

            # 5. レスポンスを返す。
            #   (1) response 関数が呼び出される。
            #   (2) response 関数から返り値が返される。
            #   (3) finally 節に移動
            #   (4) ctx.auto_pop(error) が実行される。
            #   (5) return される。
            return response(environ, start_response)
        finally:
            ...

            # 4. コンテキストを pop
            ctx.auto_pop(error)

順番が少しややこしいです。 try -> except -> finally の順に実行されています。 ただし return 文を除きます。

# 5.3. アプリケーションコンテキストはどこで?

ここです。リクエストコンテキストをプッシュした ctx.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 しています。

app_ctx = _app_ctx_stack.top はなんでしょうか? グローバル変数にスタックを代入して、コンテキストを積み込んでいるようです。

flask.current_app とか、flask.request のように、現在実行中のアプリケーションに関する情報、現在処理中のリクエストに関する情報を、それぞれコンテキストスタックと呼ばれるスレッドローカルなスタックを使って管理しています。 スタックになっているのは Flask アプリの中から他の Flask アプリを実行するなどができるようにするためで、あるアプリの拡張をしたい場合はそのスタックの一番上にあるコンテキストを編集します。
Flaskのカスタマイズについて - Qiita (opens new window)

# 5.4. 思ったこと

個人的な感想ですが ctx.push() の中でアプリケーションコンテキストも同時に push されているとは思いませんでした。 Flask は、他のライブラリでもそうなのですが、グローバル変数をガシガシ使っていくので、 ちょっと辛い時がありますorz

# Flask の設計
ctx.push()

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

# 6. スレッドセーフ

# 6.1. マルチスレッド

WSGI 経由で関数が呼び出されるとコンテキストが生成されます。 で、実際には複数同時にアクセスがあるので、複数同時に関数が起動します。

g はグローバル変数です。どうなるでしょうか? めちゃくちゃになるらしいです。 複数同時に起動された Python 間では(正確にはスレッド間では)、 グローバル変数がちゃんと安全に変更されません。 そのままだと。

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

Context Locals - Flask (opens new window)

スレッドによって制御されているコンテクストを想像してください。 リクエストが来てサーバがスレッドを生成すると決めました (もしくは、別の)。 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 標準ライブラリ (opens new window)

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

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

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

# 6.2. 深掘りすると...

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. さらに深掘りすると...

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

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

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

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

# 7. pdb と breakpoint

どうやって場所を特定したか pdb の breakpoint を使いました。

# 7.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()

debug=True だと、ファイルの変更があった時に更新を読み込むため、そのファイルを探そうとするから、 こういうエラーが生じるとのこと。 相変わらず Stackoverflow が凄すぎる。

# 7.2 Flask を起動する。

python3 sample.py

# 7.3. ページにアクセスする。

breakpoint に達すると pdb (opens new window) が起動します。 where を打ち込みます。

(pdb)

# 7.4. where を打ち込みます。

(pdb) where

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

するとスタックトレースが表示されます。スタックトレースというのは、関数が呼び出された順番が表示されたものです。 この中を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/user/.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 (opens new window)

(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.

# おわりに

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

Flask は、グローバル変数を多用するというアンチパターンを踏んでいます。 そのため、どことどこが繋がってるか、若干わかりにくくなってしまいます。

グローバル変数を管理するコンテキストについて考えて、これに慣れていけたらと思いました。 以上になります。ありがとうございました。