Flask の Werkzeug ローカルプロキシの仕組み

Flask の requestcurrent_appgsession はグローバル変数のように見えるが、実際は Werkzeug の LocalProxy という仕組みで動作している。この仕組みを理解すると、Flask のコンテキストシステムが明確になる。

なぜ LocalProxy が必要か

マルチスレッド環境で普通のグローバル変数を使うと、スレッド間でデータが混在する。しかし Flask では複数のリクエストが並行処理されても、各スレッドが正しい request を参照できる。

from flask import request

@app.route('/a')
def route_a():
    return request.path  # '/a'

@app.route('/b')
def route_b():
    return request.path  # '/b'

# 同時にアクセスしても正しいパスが返る

LocalProxy の仕組み

LocalProxy はアクセスのたびに現在のコンテキストから実際のオブジェクトを取得する。

# 簡略化したイメージ
class LocalProxy:
    def __init__(self, lookup_func):
        self._lookup = lookup_func
    
    def __getattr__(self, name):
        obj = self._lookup()  # 毎回現在のオブジェクトを取得
        return getattr(obj, name)

request は内部的に以下のような定義になっている。

from werkzeug.local import LocalProxy

def _find_request():
    # 現在のリクエストコンテキストから request を取得
    return _request_ctx_stack.top.request

request = LocalProxy(_find_request)

LocalStack

コンテキストはスタック構造で管理されている。

from werkzeug.local import LocalStack

_request_ctx_stack = LocalStack()

# リクエスト開始時
_request_ctx_stack.push(request_context)

# リクエスト終了時
_request_ctx_stack.pop()

スタック構造により、ネストしたコンテキストも正しく処理できる。

スレッドローカル vs Greenlet

Werkzeug の Local は threading.local() を拡張しており、Greenlet(gevent など)にも対応している。

# Werkzeug の Local は以下を自動判別
# - スレッド ID
# - Greenlet ID(gevent 使用時)

_get_current_object()

LocalProxy から実際のオブジェクトが必要な場合は _get_current_object() を使う。

# シグナルに渡す場合
from flask import current_app
real_app = current_app._get_current_object()
some_signal.send(real_app)

# isinstance チェック
from flask import Flask
isinstance(current_app._get_current_object(), Flask)  # True
isinstance(current_app, Flask)  # False(LocalProxy だから)

コンテキスト外でのアクセス

コンテキスト外で LocalProxy にアクセスするとエラーになる。

from flask import request

print(request.path)  # RuntimeError: Working outside of request context

これは _find_request() がコンテキストスタックから何も見つけられないためである。この挙動により、コンテキストの誤用を早期に検出できる。

LocalProxy の仕組みを理解すれば、Flask のコンテキストシステムがなぜ安全にマルチスレッド動作できるのかが分かる。