with app.app_context(): 进入应用上下文。Working outside of application context 基本都源于在上下文之外访问上下文对象。上下文的抽象理解之后,几个真实场景能帮你把知识"焊死"。
场景一:before_request 里预加载用户。 这是 g 对象最经典的用法:每个请求开始,从 session 里读用户 id,查出用户对象放进 g,视图里直接用 g.current_user 访问,避免每个视图都重复查库。Flask-Login 内部就是这么做的(它把当前用户放进请求上下文)。你的自定义逻辑(权限标记、请求级配置)同样可以挂到 g 上。
from flask import Flask, g, session, abort app = Flask(__name__) app.secret_key = "dev-only-secret" @app.before_request def load_logged_in_user(): """每个请求进来先执行:从 session 取用户 id,查到用户挂到 g。""" user_id = session.get("user_id") if user_id is None: g.current_user = None # 未登录,视图里统一判空 return g.current_user = {"id": user_id, "name": f"用户{user_id}"} @app.route("/profile") def profile(): if g.current_user is None: abort(401) return f"你好,{g.current_user['name']}"
注意 before_request 与视图在同一个请求上下文里执行,所以这里写入 g 的值,视图函数可以直接读到。这比在每个视图里重复 session.get("user_id") 再查一遍库要干净得多,也方便后续加权限校验中间件。
场景二:请求间传数据为什么不能用全局变量。 新手常犯的错误:定义模块级全局变量存"当前用户",多用户并发时数据互相覆盖(线程 A 的用户变成线程 B 的用户)。上下文(Local)正是为了解决这个:每个线程有自己的存储空间。理解"为什么不能用普通全局变量",就理解了为什么需要上下文。
# 错误示范:全局变量会被并发请求互相覆盖 current_user = None # 模块级全局 @app.route("/login") def login(): global current_user current_user = session["user_id"] # 线程 A 写进来 # 线程 B 同时写,A 再读就拿到 B 的值 —— 串台!
# 正确做法:挂在 g 上,每个线程独立 from flask import g @app.route("/login") def login(): g.current_user = session["user_id"] # 线程私有,互不干扰 return "ok"
Flask 底层用 werkzeug.local.Local 和 LocalStack 实现:每个线程(以及异步协程)访问 request、g 时,实际是从当前线程自己的存储栈里取值。所以同一个全局变量名,在不同线程里读到的是不同内容,从机制上杜绝了串台。
场景三:测试里的上下文。 用 test_client 测试时,每个请求都会创建自己的上下文,不需要你手动管理。但如果你在测试里直接调用业务函数(不经过 HTTP),就需要手动进入上下文:with app.app_context(): 或 with app.test_request_context():。前者只有应用上下文(能访问 current_app/g),后者还有请求上下文(能访问 request/session)——按需选择。
# test_demo.py from your_app import create_app, db def test_query_without_http(): app = create_app() with app.app_context(): # 手动压入应用上下文 rows = db.session.execute("SELECT 1").fetchall() assert rows # 没有这行 with 会报 Working outside...
| 进入方式 | 能访问 | 典型用途 |
|---|---|---|
with app.app_context(): |
current_app、g | 脚本、后台任务、直接调用业务函数 |
with app.test_request_context(): |
request、session + 上面全部 | 测试路由、构造请求环境 |
| 视图函数内部 | 全部 | 正常 HTTP 请求 |
场景四:后台任务的上下文。 Celery 任务(第三章 3.6)在独立进程中运行,默认没有 Flask 上下文。在任务里访问 current_app 需要先手动压入:with app.app_context():。这是 Flask + Celery 开发中最常见的"上下文坑"之一——任务函数里突然报 Working outside of application context,就是这个原因。
from celery_app import celery @celery.task def send_welcome_email(user_id): from flask import current_app # 延迟导入避免循环依赖 with current_app.app_context(): # 关键:任务里没有自动上下文 mail_cfg = current_app.config["MAIL_SETTINGS"] # 在这里发邮件、读写数据库都安全
场景五:为什么 request 在模板里也能用。 Jinja2 模板渲染发生在请求上下文内,所以模板可以直接访问 request、session、g、url_for、get_flashed_messages——它们都是上下文提供的。这就是为什么模板里能写 {{ request.path }}。理解这一点,你就不再困惑"模板里怎么突然能用这些对象"。
{# templates/profile.html #} <p>当前路径:{{ request.path }}</p> {% if g.current_user %} <p>你好,{{ g.current_user.name }}</p> {% endif %}
下面用一张图概括请求从进入 WSGI 到响应结束,上下文对象如何压栈与弹栈:

请求上下文和应用上下文都存放在线程本地栈中:请求进入时压栈,视图和模板执行期间可用,响应返回后自动弹栈销毁。栈式结构保证了嵌套调用(比如视图里再手动 with app.app_context(): 打开子上下文)不会互相污染。
最后再强调一次排查套路:任何 "Working outside of ... context" 报错,思路都是"这个对象需要上下文,而当前代码不在上下文内"。解决方式二选一:把代码移进上下文内(视图/with 块),或手动压入上下文。分清"请求上下文"与"应用上下文"两种类型,报错信息里会明确告诉你缺的是哪一种。先看报错里缺哪个对象:缺 request/session 说明要请求上下文,缺 current_app/g 说明只要应用上下文;然后在代码里用 with app.app_context(): 或 with app.test_request_context(): 包起来,问题即可解除。