Manjusaka

Manjusaka

Flask におけるコンテキストの初探

Flask のコンテキストの初探#

皆さん、新年あけましておめでとうございます!今年の春の祭典がとても素晴らしかったので、耐えられず、少しくだらない記事を書いて皆さんを楽しませようと思います。これは以前立てた幾つかのフラグの一つでもあります。

本文#

Flask 開発をしたことがある方は、Flask に二つの概念が存在することを知っているでしょう。一つは App Context、もう一つは Request Context です。この二つは Flask において非常に独特なメカニズムです。

Flask アプリが設定を読み込み、起動を開始すると、App Context に入ります。その中では、設定ファイルにアクセスしたり、リソースファイルを開いたり、ルーティングルールを通じて URL を逆に構築したりできます。WSGI ミドルウェアが Flask アプリを呼び出すと、Request Context に入ります。その中では HTTP ヘッダーなどの操作を取得でき、同時にセッションなどの操作も行えます。

しかし、私のような初心者は、なぜこの二つのコンテキストが存在するのかをしばしば理解できません。大丈夫です、ゆっくり説明していきましょう。

予備知識#

まず、同じプロセス内で異なるスレッドのデータを隔離する必要があることを理解しておく必要があります。そのため、私たちは優先的に threading.local を選択し、データの相互隔離を実現します。しかし、ここで問題が発生します。現在の並行モデルは、従来の意味でのプロセス - スレッドモデルだけではないかもしれません。**coroutine(コルーチン)** モデルである可能性もあります。一般的には Greenlet/Eventlet です。この場合、threading.local は私たちのニーズを満たすことができません。そこで Werkzeug は独自の Local、すなわち werkzeug.local.Local を実装しました。

では、Werkzeug が独自に実装した Local は、標準の threading.local と比べて何が違うのでしょうか?私たちは最大の違いを覚えておきましょう。

前者は Greenlet が利用可能な場合、スレッド ID の代わりに Greenlet の ID を優先的に使用して Gevent または Eventlet のスケジューリングをサポートしますが、後者はマルチスレッドスケジューリングのみをサポートします。

Werkzeug はさらに二つのデータ構造を実装しています。一つは LocalStack、もう一つは LocalProxy です。

LocalStackLocal に基づいて実装されたスタック構造です。スタックの特性は後入れ先出しです。コンテキストに入ると、現在のオブジェクトがスタックにプッシュされます。そして、スタックのトップ要素を取得することもできます。これにより、現在のコンテキスト情報を取得できます。

LocalProxy は代理パターンの一種の実装です。インスタンス化の際に、callable の引数を渡します。この引数が呼び出されると、Local オブジェクトが返されます。その後のすべての操作、例えば属性呼び出しや数値計算などは、この引数が返す Local オブジェクトに転送されます。

今、皆さんはなぜ LocalProxy を使って操作するのかよくわからないかもしれません。例を見てみましょう。


from werkzeug.local import LocalStack
test_stack = LocalStack()
test_stack.push({'abc': '123'})
test_stack.push({'abc': '1234'})

def get_item():
    return test_stack.pop()

item = get_item()

print(item['abc'])
print(item['abc'])

ここで出力される値はすべて同じ 1234 ですが、私たちが達成したいのは、毎回取得する値がスタックの最新の要素であることです。この時、プロキシパターンを使用する必要があります。

from werkzeug.local import LocalStack, LocalProxy
test_stack = LocalStack()
test_stack.push({'abc': '123'})
test_stack.push({'abc': '1234'})

def get_item():
    return test_stack.pop()

item = LocalProxy(get_item)

print(item['abc'])
print(item['abc'])

これがプロキシの妙用です。

コンテキスト#

Flask は Werkzeug に基づいて実装されているため、App Context および Request Context は前述の LocalStack に基づいて実装されています。

名前からもわかるように、App Context はアプリケーションのコンテキストを表し、ログ設定やデータベース設定など、さまざまな設定情報を含む可能性があります。一方、Request Context はリクエストのコンテキストを表し、現在のリクエスト内のさまざまな情報を取得できます。例えば、ボディに含まれる情報です。

これら二つのコンテキストの定義は flask.ctx ファイル内にあり、それぞれ AppContext および RequestContext です。そして、コンテキストを構築する操作は、flask.globals ファイル内で定義された _app_ctx_stack および _request_ctx_stack にプッシュされます。前述のように、LocalStack は「スレッド」(ここでは従来の意味でのスレッドである可能性もありますし、Greenlet のようなものである可能性もあります)を隔離します。また、Flask の各スレッドは一度に一つのリクエストのみを処理するため、リクエストの隔離が可能です。

app = Flask(__name__) が Flask アプリを構築すると、App Context は自動的にスタックにプッシュされません。この時、Local Stack のスタックトップは空で、current_app も未バインドの状態です。


from flask import Flask

from flask.globals import _app_ctx_stack, _request_ctx_stack

app = Flask(__name__)

_app_ctx_stack.top
_request_ctx_stack.top
_app_ctx_stack()
# <LocalProxy unbound>
from flask import current_app
current_app
# <LocalProxy unbound>

Web の場合、リクエストが来ると、私たちはコンテキストに関連する操作を開始します。全体の流れは次の通りです。

image

さて、ここで少し問題があります:

  1. なぜ App Context と Request Context を区別する必要があるのか?

  2. なぜスタック構造を使ってコンテキストを実現するのか?

以前、松鼠オリオ先生のブログFlask のコンテキストメカニズムを読んだ時に、この問題が解決されました。

これら二つのアプローチは、複数の Flask アプリの共存と、非 Web ランタイムにおけるコンテキストの柔軟な制御の可能性を提供します。

Flask アプリに対して app.run () を呼び出すと、プロセスはブロックモードに入り、リクエストをリッスンし始めます。この時、別の Flask アプリをメインスレッドで実行することは不可能です。では、複数の Flask アプリが共存する必要があるシーンは何でしょうか?前述のように、Flask アプリのインスタンスは WSGI アプリケーションであり、WSGI ミドルウェアはコンビネーションパターンを使用することを許可しています。例えば:


from werkzeug.wsgi import DispatcherMiddleware
from biubiu.app import create_app
from biubiu.admin.app import create_app as create_admin_app

application = DispatcherMiddleware(create_app(), {
    '/admin': create_admin_app()
})

オリオ先生の文中で、Werkzeug に内蔵されたミドルウェアが二つの Flask アプリを一つの WSGI アプリケーションに組み合わせる例が挙げられています。この場合、二つのアプリは同時に実行され、URL の違いに応じてリクエストが異なるアプリに振り分けられます。

しかし、ここで多くの人が疑問に思うのは、なぜここで Blueprint を使わないのか?

  • Blueprint は同じアプリ内で実行されます。そのため、App Context にバインドされた関連情報はすべて一貫しています。しかし、互いの情報を隔離したい場合、App Context を使用して隔離する方が、変数名などで隔離するよりも便利です。

  • ミドルウェアパターンは WSGI で許可された特性であり、言い換えれば、Flask と他の WSGI プロトコルに従う Web フレームワーク(例えば Django)を組み合わせることも可能です。

しかし、Flask の二つのコンテキストの分離のより大きな意義は、非 Web アプリケーションの場面にあります。Flask の公式ドキュメントには次のような一文があります。

アプリケーションのコンテキストが存在する主な理由は、過去に多くの機能がリクエストコンテキストに付随していたため、より良い解決策がなかったからです。Flask の設計の柱の一つは、同じ Python プロセス内に複数のアプリケーションを持つことができるということです。

この文は言い換えれば、App Context が存在する意義は、プロセス内に複数の Flask アプリが存在するシーンに対してであり、このシーンの最も一般的な例は、Flask を使ってオフラインスクリプトのコードを書くことです。

さて、Flask の非 Web アプリケーションのシーンについて話しましょう。

例えば、Flask-SQLAlchemy というプラグインがあります。
ここでの使用シーンは、まず次のようなコードがあります。

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

database = Flask(__name__)
database.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
db = SQLAlchemy(database)


class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)

    def __repr__(self):
        return '<User %r>' % self.username

ここで最初のいくつかの重要な点に注意してください。最初の点は database.config です。そうです、Flask-SQLAlchemy は現在のアプリから対応する設定情報を取得してデータベース接続を確立します。アプリを渡す方法は二つあります。一つ目は、上の図のように直接 db = SQLAlchemy(database) とすることです。これは理解しやすいです。二つ目は、渡さない場合、Flask-SQLAlchemy は current_app を通じて現在のアプリを取得し、対応する設定を取得して接続を確立します。
では、なぜこの二つ目の方法が存在するのでしょうか?

シーンを考えてみましょう。今、二つのデータベース設定が異なるアプリが共用するモデルがあるとします。どうすればいいでしょうか?実はとても簡単です。

まず、モデルファイルを作成します。例えば data/user_model.py としましょう。

from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()


class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)

    def __repr__(self):
        return '<User %r>' % self.username

では、アプリケーションファイルでは次のように書くことができます。

from data.user_model import User
database = Flask(__name__)
database.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
with database.app_context():
    db.init_app(current_app)
    db.create_all()
    admin = User(username='admin', email='[email protected]')
    db.session.add(admin)
    db.session.commit()
    print(User.query.filter_by(username="admin").first())

database1 = Flask(__name__)
database1.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test1.db'
with database1.app_context():
    db.init_app(current_app)
    db.create_all()
    admin = User(username='admin_test', email='[email protected]')
    db.session.add(admin)
    db.session.commit()
    print(User.query.filter_by(username="admin").first())

こうすることで、アプリコンテキストを通じて、Flask-SQLAlchemy は current_app を通じて現在のアプリを取得し、関連する設定情報を取得できることがわかります。

この例はまだ十分ではありません。もう一つの例を考えてみましょう。

from flask import Flask, current_app
import logging

app = Flask("app1")
app2 = Flask("app2")

app.config.logger = logging.getLogger("app1.logger")
app2.config.logger = logging.getLogger("app2.logger")

app.logger.addHandler(logging.FileHandler("app_log.txt"))
app2.logger.addHandler(logging.FileHandler("app2_log.txt"))

with app.app_context():
    with app2.app_context():
        try:
            raise ValueError("app2 error")
        except Exception as e:
            current_app.config.logger.exception(e)
    try:
        raise ValueError("app1 error")
    except Exception as e:
        current_app.config.logger.exception(e)

このコードは非常に明確で、現在のコンテキスト内のアプリのロガーを取得してログを出力することを示しています。また、このコードは、なぜスタックのようなデータ構造を使用してコンテキストを維持する必要があるのかを明確に示しています。

まず、app_context() のソースコードを見てみましょう。


    def app_context(self):
        """アプリケーションをバインドするだけです。アプリケーションが現在のコンテキストにバインドされている限り、:data:`flask.current_app` はそのアプリケーションを指します。リクエストコンテキストがプッシュされると、アプリケーションコンテキストが自動的に作成されます。

        使用例::

            with app.app_context():
                ...

        .. versionadded:: 0.9
        """
        return AppContext(self)

うん、とてもシンプルです。AppContext オブジェクトを構築して返すだけです。そして、関連するコードを見てみましょう。


class AppContext(object):
    """アプリケーションコンテキストは、アプリケーションオブジェクトを暗黙的に現在のスレッドまたはグリーンレットにバインドします。これは、:class:`RequestContext` がリクエスト情報をバインドするのと似ています。アプリケーションコンテキストは、リクエストコンテキストが作成されると自動的に作成されますが、アプリケーションが個々のアプリケーションコンテキストの上にない場合です。
    """

    def __init__(self, app):
        self.app = app
        self.url_adapter = app.create_url_adapter(None)
        self.g = app.app_ctx_globals_class()

        # リクエストコンテキストのように、アプリコンテキストは複数回プッシュできますが、基本的な「参照カウント」だけで追跡できます。
        self._refcnt = 0

    def push(self):
        """アプリコンテキストを現在のコンテキストにバインドします。"""
        self._refcnt += 1
        if hasattr(sys, 'exc_clear'):
            sys.exc_clear()
        _app_ctx_stack.push(self)
        appcontext_pushed.send(self.app)

    def pop(self, exc=_sentinel):
        """アプリコンテキストをポップします。"""
        try:
            self._refcnt -= 1
            if self._refcnt <= 0:
                if exc is _sentinel:
                    exc = sys.exc_info()[1]
                self.app.do_teardown_appcontext(exc)
        finally:
            rv = _app_ctx_stack.pop()
        assert rv is self, 'ポップしたアプリコンテキストが間違っています。 (%r ではなく %r)' \
            % (rv, self)
        appcontext_popped.send(self.app)

    def __enter__(self):
        self.push()
        return self

    def __exit__(self, exc_type, exc_value, tb):
        self.pop(exc_value)

        if BROKEN_PYPY_CTXMGR_EXIT and exc_type is not None:
            reraise(exc_type, exc_value, tb)

うーん、まず push メソッドは自分自身を _app_ctx_stack にプッシュし、pop メソッドは自分自身をスタックのトップからポップします。そして、これら二つのメソッドの意味は非常に明確です。コンテキストマネージャに入るときに自分自身をプッシュし、コンテキストマネージャから出るときに自分自身をポップします。

私たちはスタックの特性を知っています。すなわち、後入れ先出しであり、スタックのトップは常に最新の挿入された要素です。current_app のソースコードを見てみましょう。


def _find_app():
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return top.app
    
current_app = LocalProxy(_find_app)

うん、非常に明確です。これは現在のスタックのトップの要素を取得し、関連する操作を行います。

このようにスタックを操作し続けることで、current_app が現在のコンテキスト内のアプリを取得できるようになります。

追加の説明: g#

g も私たちがよく使うグローバル変数の一つです。最初、この変数はリクエストコンテキストにバインドされていました。しかし、0.10 以降、g はアプリコンテキストにバインドされるようになりました。なぜこうなったのか、わからない方もいるかもしれません。

まず、g は何のために使われるのかを説明します。

公式のコンテキストに関するセクションには、次のような説明があります。

アプリケーションコンテキストは必要に応じて作成され、破棄されます。スレッド間で移動することはなく、リクエスト間で共有されることもありません。そのため、データベース接続情報やその他の重要な情報を保存するのに最適な場所です。内部スタックオブジェクトは flask._app_ctx_stack と呼ばれています。拡張機能は、十分にユニークな名前を選択すれば、最上位レベルに追加情報を保存することができますが、ユーザーコード用に予約された flask.g オブジェクトには保存しないでください。

要するに、データベース設定やその他の重要な設定情報はアプリオブジェクトにバインドされます。しかし、ユーザーコードのように、データを関数間で渡したくない場合や、いくつかの変数を渡す必要がある場合は、g にバインドできます。

また、前述のように、Flask は単なる Web フレームワークとしてだけでなく、非 Web の場面でも使用できます。この場合、g がリクエストコンテキストに属していると、g を使用するためには手動でリクエストを構築する必要があり、これは明らかに不合理です。

最後に#

大晦日にこの記事を書き、今発表します。私のくだらない記事も救いようがありません。Flask のコンテキストメカニズムは、その最も重要な特徴の一つです。コンテキストメカニズムを適切に利用することで、私たちはより多くの場面で Flask をより良く活用できます。さて、今回のくだらない記事の執筆活動はここで終了します。皆さんが私に悪臭のする卵を投げないことを願っています!それでは、新年おめでとうございます!

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。