在用 Flask 写视图代码的时候经常会用到诸如 requestsessiong 这类看上去是全局变量的东西,这些变量往往带有处理请求需要用到的比如请求对象、会话参数等信息,允许我们访问一些请求信息或者全局的信息。而实现这些的就是 Flask 特殊的 Context(上下文)机制。

本地上下文

from flask import Flask, request

app = Flask(__name__)

@app.route('/', methods=['GET'])
def index():
    name = request.args.get('name', 'visitor')
    return '<h1>Hello, %s</h2>' % name

上面的视图函数中,request 看上去是一个全局变量,但这在多线程环境中显然不合理。当一个请求被 WSGI 服务器转发给 Flask 应用时,一个工作线程(亦或是进程、协程)只能处理一个请求。因此 Flask 使用了一个 context local 的概念,使得在请求处理期间,请求数据可被认为是该工作线程的全局数据。实现原理类似 Python 中 thread local,即线程本地的机制,带有这种机制的对象可以做到根据线程隔离状态,保证每个线程的操作都不会影响到其他线程。Flask 依赖的 Werkzeug 则是自己实现了一套类似的机制,只需要根据字典的原理,用不同线程的 ID 作为 Key 存储每个线程独立的数据即可。

Werkzeug 的实现

>>> from flask import request
>>> type(request)
<class 'werkzeug.local.LocalProxy'>

可以看到 request 其实是一个代理,而这个代理指向的是什么,这就需要一睹 Flask 和其依赖的 Werkzeug 的源码了。以下以 request 为例剖析一番。

省略部分代码的 flask.globas.py

from functools import partial
from werkzeug.local import LocalStack, LocalProxy

_request_ctx_err_msg = '''\\
Working outside of request context.
......\\
'''

def _lookup_req_object(name):
    top = _request_ctx_stack.top
    if top is None:
        raise RuntimeError(_request_ctx_err_msg)
    return getattr(top, name)
    
    _request_ctx_stack = LocalStack()

request = LocalProxy(partial(_lookup_req_object, 'request'))

可见 request 是一个 LocalProxy 实例,这个实例是一个代理。而 _request_ctx_stack 则是一个 LocalStack 的实例,在这里是一个单例的用于存储全局请求上下文数据的栈数据结构。从代码中不难看出,request 这个代理永远指向 _request_ctx_stack 的栈顶的请求上下文,并从请求上下文中取得 request 的实际数据。此处的 request 是一个代理,并非原始的请求对象,要获得真实的请求对象,则需要调用 request._get_current_object 方法。

LocalStackLocalProxy 的实现则是在 werkzeug.local.py 中。当中的 Local 类便是上下文数据实际存储的类,当其实例化后,会有一个名为 __storage__ 的字典属性被初始化,当调用 __setattr__ 方法时候,会在 __storage__ 这个字典中存储一个以当前线程的唯一标识符为 Key 的字典,当中存储着实际的数据。当前线程优先使用 greenlet ,其次是 _thread(Python2 中为 thread)。LocalStack 就是 Local 存储栈结构数据的一个实现,其 _local 属性就是 Local 的一个实例,调用 push 方法会将一个对象 append 到 _local__storage__ 中一个 Key 名为 stack 的列表中,该列表就是一个栈数据结构了,调用 top 属性(property 装饰过的函数)会返回该列表末尾 [-1](即栈顶)的元素。

>>> from werkzeug.local import LocalStack, LocalProxy
>>> ls = LocalStack()
>>> ls._local.__storage__
{}
>>> ls.push('something')
['something']
>>> ls._local.__storage__
{<greenlet.greenlet object at 0x10b86b210>: {'stack': ['something']}}
>>> ls.top
'something'
>>> def _lookup_ls_top():
...     top_ = ls.top
...     if top_ is None:
...         raise RuntimeError('No top!')
...     return top_
...
>>> top = LocalProxy(_lookup_ls_top)
>>> top
'something'
>>> type(top)
<class 'werkzeug.local.LocalProxy'>
>>> type(top._get_current_object())
<class 'str'>
>>> ls.pop()
'something'
>>> top
<LocalProxy unbound>
>>>

上下文生命周期

上下文是有生命周期的,当一个 Flask 应用开始处理一个请求时,会将一个请求上下文(flask.ctx.RequestContext 类)和一个应用上下文(flask.ctx.AppContext)入栈,每个上下文都是线程隔离的。当请求处理结束并生成一个响应后,请求上下文和应用上下文会被从栈顶弹出,结束生命周期。此外,Local 类也提供一个 __release_local,用于销毁当前线程在 __storage__ 中存储的所有数据,而不会影响到其他线程(LocalStack 也提供)。Flask 全局的 LocalStack 栈有 _request_ctx_stack 以及 _app_ctx_stack,依此特性你也可以自己维护实现一个类似的单例栈结构用于存储线程隔离的数据。下图展示了一个请求上下文的生命周期流程:

context lifetime

一些问题

为何设计成栈结构

因为设计成栈结构一是可以非常方便地在内部维护,可以压栈和弹出多次。在通常的请求响应过程中,当前的请求上下文和应用上下文只会有 0 个或者 1 个,但是仍有可能会有多个,例如内部重定向。与外部重定向不同的是,Flask 内部重定向会创建一个新的请求到重定向的目标,并且该请求的结果才会被作为客户端原始请求的响应体。

应用上下文的作用

Flask 支持多个应用运行在单个 WSGI Python 解释器中,并且通过诸如前辍、子域等将不同的 WSGI 请求分发到不同的应用中去,以下是一个简单的例子:

from werkzeug.wsgi import DispatcherMiddleware
from frontend_app import application as frontend
from backend_app import application as backend

application = DispatcherMiddleware(frontend, {
    '/backend':     backend
    })

此时就需要有应用上下文的概念,用以在代码逻辑中访问不同的应用。因为不同的应用有不同的配置、路由、请求钩子等。例如 url_for 函数的逻辑就依赖当前的应用环境,如果多个应用有同样的路由,url_for 返回的的内容取决于当前当前出于哪个应用的请求处理过程之中。

Refference:

Context Locals - Werkzeug

Application Dispatching - Flask