前两章你一直在用 request 却从没给它传过参数——这背后是 Flask 最「魔法」的一块:上下文。本章把这块魔法拆开:为什么会有它、request / session / current_app / g 各自管什么、怎么用 g 把当前登录用户带到每个视图里,以及为什么在脚本和后台线程里直接用它们会报 Working outside of context、该怎么修。
Flask 通常由一个进程、多个 worker 线程同时处理很多请求。每个请求都有自己独立的 request / session「上下文」,彼此不会串门——这就是「上下文」要解决的核心问题。
from flask import Flask, current_app, jsonify
app = Flask(__name__)
app.config["PAGE_SIZE"] = 10
app.json.ensure_ascii = False
# 写法 A:谁来用都得把 page_size 传给它
def build_title_a(article, page_size):
return f"{article['title']}(每页 {page_size} 条)"
# 写法 B:函数不接这个参数,直接向「当前应用」要配置
def build_title_b(article):
page_size = current_app.config["PAGE_SIZE"] # 不用传,Flask 帮你找到当前的 app
return f"{article['title']}(每页 {page_size} 条)"
@app.route("/api/article/detail/<int:aid>")
def article_detail(aid):
article = {"id": aid, "title": "静夜思"}
return jsonify({
"code": 200,
"msg": "success",
"data": {
"a": build_title_a(article, current_app.config["PAGE_SIZE"]),
"b": build_title_b(article),
},
})
from flask import Flask, jsonify, request, session
app = Flask(__name__)
app.secret_key = "dev-secret-change-me" # session 靠它签名,必须设!
app.json.ensure_ascii = False
@app.route("/api/visit", methods=["GET"])
def visit():
# 这是本次请求的信息(请求上下文)
agent = request.headers.get("User-Agent", "")
# 这是「这个浏览器」累积的信息(session:Flask 签名后存进 Cookie)
history = session.get("history", [])
history.append("静夜思")
session["history"] = history[-3:] # 只留最近 3 条
return jsonify({
"code": 200,
"msg": "success",
"data": {"agent": agent, "history": session["history"]},
})
应用上下文让 current_app 指向当前 app(不用 import 就能读配置),g 则是「一次请求的临时口袋」,常用来放当前登录用户——请求结束自动清空,不会串到下一个请求。
from flask import Flask, current_app, jsonify
app = Flask(__name__)
app.config["UPLOAD_DIR"] = "static/upload/avatar"
app.config["MAX_CONTENT_LENGTH"] = 2 * 1024 * 1024 # 上传限 2MB
app.config["PAGE_SIZE"] = 10
app.json.ensure_ascii = False
@app.route("/api/config/check", methods=["GET"])
def config_check():
# current_app 指向「现在这个 app 对象」,能读配置、读属性
return jsonify({
"code": 200,
"msg": "success",
"data": {
"debug": current_app.debug,
"upload_dir": current_app.config["UPLOAD_DIR"],
"max_content_length": current_app.config["MAX_CONTENT_LENGTH"],
"page_size": current_app.config["PAGE_SIZE"],
},
})
import functools
from flask import Flask, g, jsonify, request
app = Flask(__name__)
app.json.ensure_ascii = False
def login_required(view):
"""第 8 章会换成真正的 JWT 验签版,这里先看清 g 的用法"""
@functools.wraps(view) # 少了这行,所有端点名都会变成 wrapper
def wrapper(*args, **kwargs):
token = request.headers.get("Authorization", "").removeprefix("Bearer ")
if not token:
return jsonify({"code": 401, "msg": "请先登录", "data": None})
# 假设已经验签成功,从 token 里解出了用户
user = {"id": 3, "username": "zhangsan", "nickname": "张三"}
g.user = user # 放进本次请求的口袋
return view(*args, **kwargs)
return wrapper
@app.route("/api/favorite/list", methods=["GET"])
@login_required
def favorite_list():
# 视图函数没有任何参数,却拿得到当前用户
if g.user["id"] == 0:
return jsonify({"code": 403, "msg": "无权限", "data": None})
return jsonify({
"code": 200,
"msg": "success",
"data": {
"user": g.user["nickname"],
"list": [],
"total": 0,
"page": 1,
"page_size": 10,
},
})
request 不是一个真的对象,而是一个「代理」。你写 request.args 时,它按「当前线程」找到这个线程真正对应的请求对象再取值——所以不用你手动传参。
from flask import Flask, g, request
app = Flask(__name__)
@app.route("/api/whoami", methods=["GET"])
def whoami():
print("request 的类型:", type(request))
print("g 的类型:", type(g))
print("本次请求的 who:", request.args.get("who", "匿名"))
print("刚开始,g 里有什么:", dict(g))
g.who = request.args.get("who", "匿名")
print("赋值后,g 里有什么:", dict(g))
return "ok"
request / current_app 只在有上下文时可用。直接跑脚本、后台线程、定时任务里没有请求,就会报 Working outside of context——用 with app.app_context() 手工造一个上下文即可。
# scripts/init_data.py
from flask import current_app
from app import app # noqa: F401 (导入了 app,但没「推上台面」)
# 直接读配置:此时没有任何上下文,柜子是空的
print("上传目录:", current_app.config["UPLOAD_DIR"])
# scripts/init_data.py
from flask import request
from app import app
from models import db # 第 6 章的扩展实例,先按骨架写
def init_data():
# 手动推入应用上下文,块内的 current_app / db 都能用
with app.app_context():
print("上传目录:", app.config["UPLOAD_DIR"])
db.create_all() # 第 6 章:按 models/ 里的模型建表
print("建表完成")
def check_ctx():
# 需要「假装有个请求」时,用 test_request_context(写单元测试的常规操作)
with app.test_request_context("/api/article/list?page=2"):
print("模拟请求里的 page:", request.args.get("page"))
if __name__ == "__main__":
init_data()
check_ctx()
import functools
from flask import Flask, current_app, g, jsonify, request
app = Flask(__name__)
app.config["PAGE_SIZE"] = 10
app.config["UPLOAD_DIR"] = "static/upload/movie"
app.json.ensure_ascii = False
# 第 1 题
@app.route("/api/meta", methods=["GET"])
def meta():
return jsonify({
"code": 200,
"msg": "success",
"data": {"page_size": current_app.config["PAGE_SIZE"]},
})
# 第 2 题
def login_required(view):
@functools.wraps(view) # 保住原函数名 = 端点名,url_for 才找得到
def wrapper(*args, **kwargs):
token = request.headers.get("Authorization", "").removeprefix("Bearer ")
if not token:
return jsonify({"code": 401, "msg": "请先登录", "data": None})
g.user = {"id": 3, "username": "zhangsan", "nickname": "张三"}
return view(*args, **kwargs)
return wrapper
@app.route("/api/favorite/list", methods=["GET"])
@login_required
def favorite_list():
return jsonify({"code": 200, "msg": "success",
"data": {"user": g.user["nickname"], "list": [], "total": 0}})
@app.route("/api/user/profile", methods=["GET"])
@login_required
def user_profile():
return jsonify({"code": 200, "msg": "success",
"data": {"id": g.user["id"], "nickname": g.user["nickname"]}})
# 第 3 题(红影赏析)
def init_movies():
with app.app_context():
print("上传目录:", current_app.config["UPLOAD_DIR"])
print("准备写入 2 部电影")
# 第 6 章这里会换成 db.session.add_all([...]) + db.session.commit()
def show_batch():
with app.test_request_context("/api/movie/list?batch=2"):
print("模拟请求里的 batch:", request.args.get("batch"))
if __name__ == "__main__":
init_movies()
show_batch()
第 2 题的两问:加 functools.wraps 是为了保住原函数的名字(也就是端点名),否则多个接口的端点名都会变成 wrapper,Flask 直接启动报错、url_for 也找不到。没走装饰器读 g.user 报错,是因为 g.user 只在 login_required 里赋过值,别的请求里的 g 是全新的空口袋——稳妥写法是 getattr(g, "user", None)。第 3 题的关键:脚本运行时根本没有请求,柜子是空的,所以要么用 app.app_context() 摆上应用台面,要么再用 test_request_context() 假装有一个请求。