请求上下文¶
在请求当中,请求上下文保持对请求层级的数据的追踪。相比在请求当中把请求对象传递到每个函数当中,Flask 使用了 request 与 session 代理对象来访问请求对象。
这与 应用上下文 类似,只不过应用上下文跟踪独立于请求的应用层级数据。在请求上下文被推入时,对应的应用上下文也被推入。
上下文的目的¶
当 Flask 应用处理请求,根据从 WSGI 服务器获取到的环境,创建出相应的 Request 对象。因为 工作者 (根据不同的服务器可能为线程,进程或协程)一次只能处理一个请求,在请求当中,请求数据可以认为对工作者全局可见、Flask 用术语 上下文局部变量(context local) 来表示这种设计。
Flask 在处理请求时,会自动 推入 请求上下文。视图函数,错误处理钩子函数与其他在请求当中运行的函数可以访问指向当前请求的请求对象代理对象 request。
上下文的生命周期¶
当 Flask 应用开始处理请求时,推入请求上下文的同时也推入了 app context。当请求结束,会先弹出请求上下文,然后再弹出应用上下文。
上下文对于每个线程(或其他类型的工作单元)都是唯一的。request 不能被传递到另一个线程,因为其他线程拥有不同的上下文空间,无法识别父线程所指向的请求对象。
上下文局部变量(context locals)是通过 Python 的 contextvars 模块和 Werkzeug 的 LocalProxy 类实现的。 Python 会自动管理上下文变量的生命周期,而本地代理(local proxy)封装了这些底层接口,使数据的使用更加方便。
手动推入一个上下文¶
如果要在应用上下文之外的地方试图去获取或者使用依赖 request 的任何东西,有可能会收到这样的错误信息:
RuntimeError: Working outside of request context.
This typically means that you attempted to use functionality that
needed an active HTTP request. Consult the documentation on testing
for information about how to avoid this problem.
这通常只会在测试需要活动请求的代码时候发生。其中一个方法是使用 test client 来模拟一个完整请求。或者可以在 with 块使用 test_request_context(),所有在块中运行的代码将会可以访问由测试数据生成的 request。
def generate_report(year):
format = request.args.get("format")
...
with app.test_request_context(
"/make_report/2017", query_string={"format": "short"}
):
generate_report()
如果你在测试应用以外场景遇到这个错误,绝大多数情况下意味着这些代码应该转移到视图函数下。
了解更多如何在 Python 交互式命令行中使用请求上下文的信息,参见 使用 Shell。
上下文的运作原理¶
在处理每个请求时,都会调用 Flask.wsgi_app() 方法。它负责在请求过程中管理上下文。在内部,请求上下文和应用上下文的工作方式类似于栈(stack)。当上下文被推入时,依赖它们的代理对象就会可用,并指向在顶部元素的相关信息。
当请求开始,将创建并推入 RequestContext 对象。如果此时应用上下文不在上下文栈的顶部,将先创建 AppContext。当这些上下文被推入,代理对象 current_app、g、request 以及 session 在处理请求的线程变得可用。
在请求中,其他上下文对象推入时可能会改变代理对象的指向。虽然这不是一种常见的使用模式,但在一些高级应用中可以使用,例如实现内部重定向或将不同的应用串联在一起。
当请求被分配到视图函数,生成并发送响应,请求上下文先被弹出,然后应用上下文也被弹出。在上下文被弹出前,函数 teardown_request() 以及 teardown_appcontext() 会被执行。即使在请求被分配过程中有未处理的请求抛出,这些函数也会被执行。
回调与错误¶
Flask 在不同阶段中会分配请求,这会影响请求、响应以及错误处理。在这些阶段中,上下文处于可用状态。
Blueprint 可以为这些事件添加蓝图专属的钩子函数。当蓝图所属的路由匹配上请求,那么蓝图内的钩子函数将被执行。
在每个请求前,会调用
before_request()函数。如果其中的一个函数返回了值,那么其他函数会被跳过执行。返回值会当作响应,视图函数不会被调用。如果
before_request()函数不返回响应,被路由匹配的视图函数将被调用,返回请求。视图函数返回的值会被转换成实际的响应对象,然后传递到
after_request()函数。每个函数返回一个被修改的或者是新的响应对象。当响应被返回,在弹出上下文的过程中,会调用
teardown_request()和teardown_appcontext()函数。即使内部有一个未处理的异常抛出,这些函数也会被执行。
如果异常在清理(teardown)函数中抛出,Flask 会尝试使用 errorhandler() 函数去处理异常,返回响应。如果没有错误钩子函数,或者钩子函数本身抛出了异常,Flask 返回了通用的 500 Internal Server Error 响应。清理函数一样会调用,而且会传入异常对象。
在 debug 模式开启的时候,未处理的异常不会被转换为 500 响应,而是传递给 WSGI 服务器。这让开发服务器可以展示带有异常跟踪(trackback)的交互式调试器。
清理钩子函数¶
清理钩子函数独立于请求分配,而是在上下文被弹出的时候才调用。当在请求分配的过程中,或者在手动推入的的上下文中有未捕抓的异常,这些函数依然会被调用。这意味着不会保证在请求分配的每一部分都会首先执行。确保在编写这些函数的时候不要依赖其他钩子函数,不要假设函数不会失败。
在测试的过程中,在请求结束的时候推迟弹出上下文是一种很有用的手段,它可以让测试函数访问上下文中的数据。在 with 块中使用 test_client() 来让上下文在离开 with 块前保留。
from flask import Flask, request
app = Flask(__name__)
@app.route('/')
def hello():
print('during view')
return 'Hello, World!'
@app.teardown_request
def show_teardown(exception):
print('after with block')
with app.test_request_context():
print('during with block')
# teardown functions are called after the context with block exits
with app.test_client() as client:
client.get('/')
# the contexts are not popped even though the request ended
print(request.path)
# the contexts are popped and teardown functions are called after
# the client with block exits
信号¶
下面的信号被发送:
在
before_request()被调用前,request_started信号会被发送。在
after_request()被调用前,request_finished信号会被发送。当异常开始处理时,
got_request_exception信号会在errorhandler()被查看或调用前被发送。在
teardown_request()被调用后,request_tearing_down信号会被发送。
代理对象的注意事项¶
一些 Flask 提供的对象,是其他对象的代理。在每个工作线程中,代理对象会以相同的方式被访问。在内部实现中,代理对象指向绑定到工作者的唯一的对象。细节如本页所述。
大多数时候,这些细节无需担心。但有些时候最好还是知道这个对象实际上是一个代理:
代理对象不能冒充实际指向的对象类型。如果要进行实例检查,应当要在被代理的对象本身进行检查。
在某些要使用被代理对象的引用的时候,如发送 信号 或者向后台线程传递数据。
如果需要访问在底层被代理的对象,使用 _get_current_object() 这个方法:
app = current_app._get_current_object()
my_signal.send(app)