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 ファイル内にあり、それぞれ AppContextRequestContext です。コンテキストを構築する操作は、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 を使用して隔離する方が、変数名などで隔離するよりも便利です。

  • ミドルウェアパターンは 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 をより良く活用できます。さて、今回のくだらない記事の執筆活動はここで終了します。皆さんが私に悪臭のする卵を投げないことを願っています!そして、新年おめでとうございます!

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