码上问学问代码之理,学编程之道

第 4 章 · 应用上下文与请求上下文

前两章你一直在用 request 却从没给它传过参数——这背后是 Flask 最「魔法」的一块:上下文。本章把这块魔法拆开:为什么会有它、request / session / current_app / g 各自管什么、怎么用 g 把当前登录用户带到每个视图里,以及为什么在脚本和后台线程里直接用它们会报 Working outside of context、该怎么修。

为什么会有「上下文」这回事

一个进程,十个请求
一个进程,同时接待 N 个请求 Flask 进程:按「线程」把各自的 request / session 交给对应请求 请求1ctx请求2ctx请求3ctx请求4ctx请求5ctx请求6ctx请求7ctx请求8ctx 每个请求的 request / session 互不相通 —— 靠线程局部存储(Thread-Local)

Flask 通常由一个进程、多个 worker 线程同时处理很多请求。每个请求都有自己独立的 request / session「上下文」,彼此不会串门——这就是「上下文」要解决的核心问题。

先讲为什么后厨里同时在做十桌菜,师傅喊了一句「拿盐」——哪一锅要加盐?他必须知道此刻手上这盘是哪桌的。
Flask 也是一样:一个进程同时「手里拿着」好几个请求。你的代码里写 request.args.get("page") 时,Python 怎么知道该去翻哪一个请求的参数?
上下文就是 Flask 给你临时摆好的「当前工作台」:请求进来时,Flask 把「这个请求」「这个应用」放上台面;请求结束,台面清空。于是你在任何被调用的函数里说「request」,拿到的都是当下正处理的那个请求,不用一层层往下传参数。
动手做对比两种写法:写法 A 老老实实把要用的东西当参数传进去,写法 B 让函数自己去「当前应用」拿。结果一模一样,但 B 少传了一个参数——工具函数越多,省下来的参数就越多。
两种写法.pypython
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),
        },
    })
运行输出Python 运行结果
运行结果: $ curl -s http://127.0.0.1:5000/api/article/detail/1 | python -m json.tool --indent 2 { "code": 200, "msg": "success", "data": { "a": "静夜思(每页 10 条)", "b": "静夜思(每页 10 条)" } }
讲透:两套上下文,四个常用对象
  • 请求上下文(request context):描述「当前这个请求」——里面养着 request(请求数据)和 session(跟着这个浏览器走的一小袋数据)。只有处理请求时才存在。
  • 应用上下文(application context):描述「当前这个 app」——里面养着 current_app(当前的 app 对象,能读配置、读路径)和 g(一次请求内的临时储物袋)。
  • 两者是「套娃」关系:处理一个请求时,Flask 先推入应用上下文,再推入请求上下文。所以有 request 的地方一定有 current_app,反过来不一定——脚本里可以只有应用上下文。
  • 它们的生命周期都很短:请求结束,两者都弹出销毁。别用 g 存需要跨请求保留的数据(比如访问次数、登录态),那是数据库或缓存的活。
避坑不要在自己的 models/ 里 import request。比如在 Article.to_dict() 里写 if request.args.get("full")——看起来方便,后果是:这个模型只能在 Web 请求里用,脚本里跑不了、单元测试也跑不了,还很难查。规矩是:模型层只认数据,要做「跟请求有关」的判断就放在视图层。要读配置,用 current_app.config(应用上下文比请求上下文宽松得多)。

请求上下文:request 与 session

跟着一个请求来,跟着一个请求走
先讲为什么上一章你已经把 request 用熟了——它天生属于「某一次请求」。请求上下文里还住着另一个对象 session,它解决的是另一类问题:同一个人跨多次请求,怎么记住他?
HTTP 本身没记性,我问一句你答一句,答完就两清。于是 Flask 提供一个 session:一次请求里写进去的数据,Flask 会签名后塞进浏览器 Cookie;下次这个浏览器再来,再原样还给你。它跟的是「浏览器」,不是「请求」。
「古典文学网」里它适合放「最近浏览过的作品」这种丢了也无所谓的小事;登录态不用它——本项目按接口文档走 JWT(第 8 章)。
动手做用 session 记一次「最近浏览」,连发两次请求看看它记不记得住。注意 session 需要 secret_key,不设会直接报错。
session.pypython
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"]},
    })
运行输出Python 运行结果
运行结果: # 第一次访问:服务端返回一个带签名的 Cookie $ curl -i http://127.0.0.1:5000/api/visit HTTP/1.1 200 OK Content-Type: application/json Set-Cookie: session=eyJoaXN0b3J5IjpbIumdmeWknuaAnSJdfQ.aXk1Tw.9fYw...; HttpOnly; Path=/ {"code":200,"msg":"success","data":{"agent":"curl/8.7.1","history":["静夜思"]}} # 第二次带上这个 Cookie:服务端认出「还是你」,历史变成两条 $ curl -s -b "session=eyJoaXN0b3J5IjpbIumdmeWknuaAnSJdfQ.aXk1Tw.9fYw..." \ http://127.0.0.1:5000/api/visit | python -m json.tool --indent 2 { "code": 200, "msg": "success", "data": { "agent": "curl/8.7.1", "history": [ "静夜思", "静夜思" ] } }
讲透:session 到底是什么
  • 默认是「客户端 session」:数据不在服务器上存,而是序列化 + 用 secret_key 签名后放进浏览器 Cookie,每次请求带回来。所以服务端不需要存东西,但数据是明文可见(只是改不了),别往里塞密码、手机号。
  • 用法像普通字典:session["history"] = [...] 写、session.get("history", []) 读、session.pop("history", None) 删。
  • secret_key 必须设且必须保密。它泄露了,别人就能伪造 session。开发时可以随便写,上线要通过环境变量注入(第 5 章)。
  • Cookie 有大小限制(约 4KB),塞不下就别塞——真实项目里跨请求的数据一般进数据库或 Redis。
  • 本项目的登录态不用 session,用 JWT:接口文档写的是 Authorization: Bearer <token>。因为前后端分离时前端可能在 5173、后端在 5000,跨域带 Cookie 要额外配置一堆东西,而 JWT 放在请求头里干净利落。
避坑忘了 app.secret_key 就用 session,一访问就报 RuntimeError: The session is unavailable because no secret key was set。别把 JWT 和 session 搞混:JWT 是「服务端发给前端、前端自己存、请求头带回来」;session 是「服务端签名后塞进 Cookie、浏览器自动带」。本课程两者都会讲到,但项目里按接口文档用 JWT。

应用上下文:current_app 与 g

读配置不再靠 import
应用上下文:current_app 与 g app 对象 配置、扩展、蓝图都挂在它身上 SECRET_KEY · SQLALCHEMY_URI g 对象 一次请求的「临时口袋」 放当前登录用户 g.user 应用上下文 current_app.config['X'] 不 import app 也能读配置 g.user = 当前用户 请求结束就清空,不串请求

应用上下文让 current_app 指向当前 app(不用 import 就能读配置),g 则是「一次请求的临时口袋」,常用来放当前登录用户——请求结束自动清空,不会串到下一个请求。

先讲为什么在第 1 章的项目骨架里,我们把配置放进了 config.py。那别的文件怎么用它?最直接的写法是 from app import app 然后 app.config[...]——但这会制造循环 import:app.py 要 import views/article.py 来注册路由,而 views/article.py 又要 import app.py 里的 app,两边互相等,项目就起不来了。
current_app 就是为解这个死结准备的:它不 import 任何东西,只是在你运行时去「当前应用」那儿取。models/、views/、工具函数都能安全地用。
动手做写一个「配置体检」接口,把 current_app 里的几个关键配置读出来。以后联调时,「后端到底读的是哪份配置」一眼就能确认。
config_check.pypython
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"],
        },
    })
运行输出Python 运行结果
运行结果: $ curl -s http://127.0.0.1:5000/api/config/check | python -m json.tool --indent 2 { "code": 200, "msg": "success", "data": { "debug": true, "upload_dir": "static/upload/avatar", "max_content_length": 2097152, "page_size": 10 } }
讲透:current_app 与 g 的分工
  • current_app = 当前的应用对象。常用三件事:读配置(current_app.config["X"])、判断环境(current_app.debug)、拿目录(current_app.static_folder、current_app.root_path)。它在整个请求期间都存在,但你只读不写。
  • g = 一次请求内共享数据的临时口袋。同一个请求里,视图函数、装饰器、工具函数拿到的都是同一个 g;请求一结束就清空。下一节专门讲它。
  • 一句话记住区别:要问「应用/配置是什么」→ current_app;要放「这次请求算出来的东西」→ g。
  • 它们都属于应用上下文,不属于请求上下文。所以严格说,current_app 在「只有应用上下文」的脚本里也能用——这正是第 6 节要用的东西。

g 对象:一次请求的临时口袋

把当前登录用户放进去
先讲为什么第 8 章要做登录鉴权时,会遇到一个很现实的问题:「当前用户是谁」要在很多地方用到——装饰器里验完 token 才知道,视图函数里要用,写日志、判断权限也要用。一路当参数传下去?函数签名会越来越长,而且中间很多层根本用不上这个参数。
Flask 给的方案是 g:装饰器验完 token,把用户塞进 g.user;同一个请求里任何地方想用,直接读 g.user 就行。请求结束,口袋自动清空,不会串到下一个请求。
动手做写一个简化的 @login_required 装饰器:校验请求头里的 token,验过就把用户放进 g.user,然后放行。视图函数自己不接任何参数,却知道「当前用户是谁」。
login_required.pypython
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,
        },
    })
运行输出Python 运行结果
运行结果: $ curl -s http://127.0.0.1:5000/api/favorite/list | python -m json.tool --indent 2 { "code": 401, "msg": "请先登录", "data": null } $ curl -s http://127.0.0.1:5000/api/favorite/list \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...." | python -m json.tool --indent 2 { "code": 200, "msg": "success", "data": { "user": "张三", "list": [], "total": 0, "page": 1, "page_size": 10 } }
讲透:g 的三条规矩
  • ① 只装「本次请求算出来的东西」:当前用户、本次查到的数据库连接、本次请求的临时开关。不要装需要跨请求保留的数据——请求一结束它就没影了。
  • ② 用之前先想好默认值:g.user 只在 login_required 里赋值。没走那个装饰器的接口去读 g.user,会报 AttributeError。稳妥写法是 getattr(g, "user", None)。
  • ③ 别用 g 当全局变量:它每个请求一份,所以写法上看着像全局、实际上很安全;反过来,如果你把「应该全局共享」的缓存塞进 g,就会每次请求都重新算——白忙。
  • 它比 session 更适合放用户对象:session 的数据要在网络上传来传去、还有大小限制;g 只活在服务器内存里,本次请求用完就扔,一点不浪费。
  • 第 8 章的完整链路就是这样:请求头带 token → @login_required 验签 → 把用户放进 g.user → 视图里直接用 g.user["id"] 查收藏、判断权限。
避坑自己写装饰器时一定要加 @functools.wraps(view)。不加的后果很隐蔽:被装饰的视图函数名字变成了 wrapper,Flask 用函数名当端点名,于是好几个接口的端点名全叫 wrapper,启动时直接报 AssertionError: View function mapping is overwriting an existing endpoint function: wrapper;就算只有一个,url_for("favorite_list") 也会报 BuildError。加一行就全好了。

不传参数,为什么也能访问

代理对象与线程局部存储
request 是「代理」,自动按当前线程取值 代码里写 request.args request(代理) 不存数据,只负责转发 线程 A → 请求A 的真实 request 线程 B → 请求B 的真实 request 按当前线程查 所以「不传参数也能用 request」——Flask 用代理 + 线程局部, 在运行时把「当前线程」对应的请求塞给你

request 不是一个真的对象,而是一个「代理」。你写 request.args 时,它按「当前线程」找到这个线程真正对应的请求对象再取值——所以不用你手动传参。

先讲为什么到这里你可能一直有个疑问:from flask import request 明明只 import 了一次,为什么每次请求它都能知道「这次是谁发的」?如果是普通全局变量,多线程下早就串了。
答案是:你 import 进来的 request 不是一个真正的请求对象,而是一个「代理人」(代理对象)。它自己不存数据,每次你访问它的属性,它就去「当前线程的储物柜」里把真正的对象取出来。
Flask 给每个线程配了独立的柜子(这叫线程局部存储),请求进来时把该请求推进本线程的柜子。所以十个线程同时处理十个请求,各找各的柜子,绝不会互相串。
动手做亲眼看一下:打印 request 和 g 的类型,都是 LocalProxy;再连发两次请求,观察 g 这个小口袋每次都是全新的空口袋。
proxy_demo.pypython
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"
运行输出Python 运行结果
运行结果: $ curl -s "http://127.0.0.1:5000/api/whoami?who=A" # 服务端终端打印: request 的类型: <class 'werkzeug.local.LocalProxy'> g 的类型: <class 'werkzeug.local.LocalProxy'> 本次请求的 who: A 刚开始,g 里有什么: {} 赋值后,g 里有什么: {'who': 'A'} $ curl -s "http://127.0.0.1:5000/api/whoami?who=B" # 又一次请求,g 又是全新的空口袋,不会残留 A request 的类型: <class 'werkzeug.local.LocalProxy'> g 的类型: <class 'werkzeug.local.LocalProxy'> 本次请求的 who: B 刚开始,g 里有什么: {} 赋值后,g 里有什么: {'who': 'B'}
讲透:三句话说完这套机制
  • 代理对象(LocalProxy)只是个「前台」:你写 request.args,它转发给柜子里真正的 Request 对象。所以你用起来和普通对象没区别,但它背后永远指向「当前的」那个。
  • 线程局部存储 = 每个线程一个独立柜子:请求开始时 Flask 把「当前请求 / 当前应用」推进去,请求结束时弹出。柜子是按线程分的,所以多线程不会互相污染。
  • 好处与代价:好处是代码干净——工具函数不用一路带着 request 当参数;代价是你必须清楚「我现在是不是在处理请求」,不然就会踩到下一节的坑。
  • 不用背 API:日常写业务代码,你只需要记住「在视图函数里,这四个对象随便用;出了视图,先想想有没有上下文」。源码层面的实现细节,用到时再翻。

常见坑:Working outside of context

脚本、后台线程、定时任务
常见坑:Working outside of context ✓ 正常请求里 Flask 自动创建上下文 request / current_app 都能用 ✗ 没有请求的地方 直接跑脚本 / 后台线程 / 定时任务 RuntimeError: Working outside of context 解决办法:手工造一个应用上下文 with app.app_context(): # 之后就能用 current_app / g 写脚本、跑定时任务时套上它即可

request / current_app 只在有上下文时可用。直接跑脚本、后台线程、定时任务里没有请求,就会报 Working outside of context——用 with app.app_context() 手工造一个上下文即可。

先讲为什么上下文是「处理请求期间」才存在的台面。可你的代码不只在处理请求时运行:初始化数据库的脚本、跑批的定时任务、往队列里塞的后台线程——这些时候根本没有请求,柜子是空的。这时 request 这个代理人去开柜子,发现是空的,只能报错:
RuntimeError: Working outside of request context(请求上下文没搭起来)或 Working outside of application context(应用上下文没搭起来)。看到这句话不用慌,它不是你的逻辑错了,而是「台面没摆」。
动手做先故意踩一次坑:写个初始化脚本,直接读 current_app,然后运行它,把报错读一遍。
scripts/init_data.py(错误写法)python
# scripts/init_data.py
from flask import current_app

from app import app            # noqa: F401  (导入了 app,但没「推上台面」)

# 直接读配置:此时没有任何上下文,柜子是空的
print("上传目录:", current_app.config["UPLOAD_DIR"])
运行输出Python 运行结果
运行结果: $ python scripts/init_data.py Traceback (most recent call last): File "/Users/me/pyweb/hiflask/scripts/init_data.py", line 6, in <module> print("上传目录:", current_app.config["UPLOAD_DIR"]) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/me/pyweb/hiflask/venv/lib/python3.11/site-packages/werkzeug/local.py", line 348, in __getattr__ return getattr(self._get_current_object(), name) File "/Users/me/pyweb/hiflask/venv/lib/python3.11/site-packages/flask/globals.py", line 52, in _find_app raise RuntimeError(_app_ctx_err_msg) RuntimeError: Working outside of application context. This typically means that you attempted to use functionality that needed the current application. To solve this, set up an application context with app.app_context(). See the documentation for more information.
动手做修法也很直接:自己把台面摆上。用 with app.app_context(): 包住需要上下文的代码,缩进块里 current_app、g、数据库扩展就都能用了。需要「假装有一个请求」时(写测试最常用),换成 app.test_request_context()。
scripts/init_data.py(正确写法)python
# 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()
运行输出Python 运行结果
运行结果: $ python scripts/init_data.py 上传目录: static/upload/avatar 建表完成 模拟请求里的 page: 2
讲透:一张对照表
  • 在视图函数里:什么都不用做,Flask 已经摆好台面,四个对象随便用。
  • 在脚本 / 定时任务里:只要应用上下文 → with app.app_context():。这是第 6 章建表、造测试数据时天天要写的。
  • 需要模拟一次请求(写测试):with app.test_request_context("/api/article/list?page=2"):,这样 request 也有了。
  • 在后台线程里:线程的柜子是空的,请求上下文不会跟着线程走。所以在线程里要么先 with app.app_context():,要么在启动线程前就把需要的数据取出来当参数传进去——后者通常更简单,也更安全。
  • 看报错就能定位:Working outside of application context → 缺应用上下文,用 app.app_context();Working outside of request context → 缺请求上下文,用 test_request_context(),或者想想这里是不是根本不该用 request。
避坑app.app_context() 只解决「应用」这一半,不会给你一个请求。在脚本里用它之后 current_app 好使了,但 request 依然报 Working outside of request context。第二个坑更隐蔽:在视图里起线程、把 g 或 request 传进去用——主请求很快结束、上下文销毁,子线程再去访问就报错,而且报错位置跟真正的起因隔得老远。稳妥做法:线程启动前把 g.user["id"] 这类值取成普通变量,再传给线程。
动手练习
1用自己的话写清楚:request、session、current_app、g 这四个对象各自属于哪套上下文、各自适合放什么。然后写一个接口 /api/meta,用 current_app 读出 PAGE_SIZE 并返回。
2写一个 @login_required 装饰器,把当前用户放进 g.user;再写两个视图 /api/favorite/list(读 g.user 返回昵称)和 /api/user/profile(读 g.user 返回资料)。说明:为什么必须加 functools.wraps?为什么没走装饰器的接口读 g.user 会报错?
3迁移练习:「红影赏析」要给 movie 表灌初始数据。请写一个 scripts/init_movies.py:在应用上下文里读配置,打印出「准备写入 2 部电影」,然后用 test_request_context 模拟一次请求,把 request.args 里的 batch 参数打印出来。最后说说:为什么这个脚本不能直接读 request?
参考答案
练习答案.pypython
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()
运行输出Python 运行结果
运行结果: $ curl -s http://127.0.0.1:5000/api/meta | python -m json.tool --indent 2 { "code": 200, "msg": "success", "data": { "page_size": 10 } } $ curl -s http://127.0.0.1:5000/api/favorite/list \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...." | python -m json.tool --indent 2 { "code": 200, "msg": "success", "data": { "user": "张三", "list": [], "total": 0 } } $ python scripts/init_movies.py 上传目录: static/upload/movie 准备写入 2 部电影 模拟请求里的 batch: 2

第 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() 假装有一个请求。

本章小结