Flask 上下文
Flask 上下文
Flask 中定义了两种上下文,分别为请求上下文和应用上下文,其中前者包括了 current_app (当前的应用对象)、g (处理请求时用作临时存储的对象);后者包括了 request (请求对象,封装了 HTTP 请求的内容)、session (用于存储请求之间需要记住的值)。
Flask 分发请求之前会激活请求上下文,请求处理完成后再将其删除,为了支持多个 app,Flask 中的 Context 是通过栈来实现。
简介
实际上所谓的上下文,在该场景下就是包括了一次请求所包含的信息,包括了从客户(一般是浏览器)发送过来的数据,例如,登陆时使用的用户名密码;以及在中间处理过程中生成的数据,例如,每次请求时我们可能会需要新建一个数据库链接。
Flask 会在接收每次请求的时候将参数自动转换为相应的对象,也就是 request、session,一般来说上下文传递可以通过参数进行,这也就意味这每个需要该上下文的函数都要多增加一个入参,为了解决这一问题,Flask 提供了一个类似于全局变量的实现方式(如下会讲到这一参数是线程安全的)。
在多线程服务器中,通过线程池处理不同客户的不同请求,当收到请求后,会选一个线程进行处理,请求的临时对象(也就是上下文)会保存在该线程对应的全局变量中(通过线程 ID 区分),这样即不干扰其他线程,又使得所有线程都可以访问。
Flask 上下文对象
如上所述,Flask 有两种上下文,分别是:
RequestContext 请求上下文
- Request 请求的对象,会封装每次的 Http 请求 (environ) 的内容;
- Session 会根据请求中的 cookie,重新载入该访问者相关的会话信息。
AppContext 程序上下文
- g 处理请求时用作临时存储的对象,每次请求都会重设这个变量;
- current_app 当前激活程序的程序实例。
生命周期
- current_app 的生命周期最长,只要当前程序实例还在运行,都不会失效。
- request 和 g 的生命周期为一次请求期间,当请求处理完成后,生命周期也就完结了。
- session 就是传统意义上的 session,只要还未失效(用户未关闭浏览器、没有超过设定的失效时间),那么不同的请求会共用同样的 session。
线程安全
首先我们看看 request 是如何实现的,实际上之所以有 request 就是为了在多线程(或者协程)的环境下,各个线程可以使用各自的变量,不至于会混乱,接下来我们看看具体是如何实现的。
简介
实际上,Python 提供了同样类似的线程安全变量保存机制,也就是 threading.local() 方法,而在 flask 中,使用的是 werkzeug 中的 Local 实现的,详细可以参考 werkzeug.pocoo.org/docs/local 。
总体来说,werkzeug 提供了与 threading.local() 相同的机制,不过是 threading 只提供了线程的安全,对于 greenlet 则无效。
flask 定义
首先,查看源码目录下的 init.py 文件,可以发现这些变量实际上是在 globals.py 中定义,然后只是在 init.py 中引入的,其定义如下。
def _lookup_req_object(name):
top = _request_ctx_stack.top
if top is None:
raise RuntimeError(_request_ctx_err_msg)
return getattr(top, name)
# context locals
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))
g = LocalProxy(partial(_lookup_app_object, 'g'))
其中,partial() 函数就是函数调用时,如果有多个参数,但其中的一个参数已经知道了,我们可以通过这个函数重新绑定一个新函数,然后去调用这个新函数,如果有默认参数的话,也可以自动对应。详见 偏函数。
而上述的代码,实际就定义了最终调用的是 getattr(top, ‘request’) 、 getattr(top, ‘session’) 。
如上,我们会发现 g、request、session 的调用方式都是一样的,最后都通过 getattr(top, name) 获取,这也就意味着有一个上下文对象同时保持了上述的三个对象。
另外,使用时,我们只要一处导入 request,在任何视图函数中都可以使用 request,关键是每次的都是不同的 request 对象,说明获取 request 对象肯定是一个动态的操作,不然肯定都是相同的 request。
之所以可以做到这样,主要就是 _lookup_req_object() 和 LocalProxy 组合完成的。
LocalProxy
LocalProxy 是 werkzeug/local.py 中定义的一个代理对象,在此处的作用是将所有的请求都发给内部的 _local 对象,如下的 __slots__
用来表示所有的实例都只有一个实现。
class LocalProxy(object):
__slots__ = ('__local', '__dict__', '__name__')
def __init__(self, local, name=None):
# 在上述的request中传递的就是_lookup_req_object,如果 传递给 _lookup_req_object 的参数是'request',那么 _lookup_req_object() 就是 request 对象
object.__setattr__(self, '_LocalProxy__local', local)
object.__setattr__(self, '__name__', name)
def _get_current_object(self):
# _lookup_req_object 中没有 __release_local__,直接进入该分支
if not hasattr(self.__local, '__release_local__'):
return self.__local()
try:
return getattr(self.__local, self.__name__)
except AttributeError:
raise RuntimeError('no object bound to %s' % self.__name__)
def __getattr__(self, name):
if name == "__members__":
return dir(self._get_current_object())
return getattr(self._get_current_object(), name)
当调用 request.method 时会调用 __getattr__()
函数,而实际上 request 对任何方法的调用,最终都会转化为对 _lookup_req_object() 返回对象的调用。
上述的 request.method 会调用 _request_ctx_stack.top.request.method 。
LocalStack
既然每次 request 都不同,要么调用 top = _request_ctx_stack.top 返回的 top 不同,要么 top.request 属性不同,在 flask 中每次返回的 top 是不一样的,所以 request 的各个属性都是变化的。
现在需要看看 _request_ctx_stack = LocalStack() 了,LocalStack 其实就是简单的模拟了堆栈的基本操作,包括 push、top、pop,其内部保存了与线程相关联的本地变量,从而使其在多线程中 request 不混乱。
LocalStack 部分源码,实现了 push
pop
pop
方法, 对 local 的操作
class LocalStack(object):
def __init__(self):
self._local = Local()
def push(self, obj):
"""Pushes a new item to the stack"""
rv = getattr(self._local, "stack", None)
if rv is None:
self._local.stack = rv = []
rv.append(obj)
return rv
def pop(self):
"""Removes the topmost item from the stack, will return the
old value or `None` if the stack was already empty.
"""
stack = getattr(self._local, "stack", None)
if stack is None:
return None
elif len(stack) == 1:
release_local(self._local)
return stack[-1]
else:
return stack.pop()
@property
def top(self):
"""The topmost item on the stack. If the stack is empty,
`None` is returned.
"""
try:
return self._local.stack[-1]
except (AttributeError, IndexError):
return None
Local
Local 部分源码
class Local(object):
__slots__ = ("__storage__", "__ident_func__")
def __init__(self):
object.__setattr__(self, "__storage__", {})
object.__setattr__(self, "__ident_func__", get_ident)
def __getattr__(self, name):
try:
return self.__storage__[self.__ident_func__()][name]
except KeyError:
raise AttributeError(name)
def __setattr__(self, name, value):
ident = self.__ident_func__()
storage = self.__storage__
try:
storage[ident][name] = value
except KeyError:
storage[ident] = {name: value}
def __delattr__(self, name):
try:
del self.__storage__[self.__ident_func__()][name]
except KeyError:
raise AttributeError(name)
__storage__
为内部保存的数据,key 是 线程或协程的 id ,也就是根据线程的标示符返回对应的值。
__storage__ 的数据结构
__storage__ = {
ident1 : {
'stack' : [ctx1,ctx2,...]
},
ident2 : {
'stack' : [ctx1,ctx2,...]
},
...
}
源码分析
分析上下文是如何实现的。
上下文对象定义
应用上下文和请求上下文在 ctx.py 中定义,内容如下。
class RequestContext(object):
def __init__(self, app, environ, request=None):
self.app = app
if request is None:
request = app.request_class(environ)
self.request = request
self.url_adapter = app.create_url_adapter(self.request)
self.flashes = None
self.session = None
RequestContext 类是请求上下文,里边保存了 app(当前对象的引用)与常用的 request , session
class AppContext(object):
def __init__(self, app):
self.app = app
self.url_adapter = app.create_url_adapter(None)
self.g = app.app_ctx_globals_class()
AppContext 类即是应用上下文,里面保存了 app (当前应用对象的引用)、g (用来保存需要在每个请求中需要用到的请求内全局变量) 。
流程
入口
flask是遵循WSGI接口的web框架,因此它会实现一个类似如下形式的函数以供服务器调用:
def application(environ, start_response): #一个符合wsgi协议的应用程序写法应该接受2个参数
start_response('200 OK', [('Content-Type', 'text/html')]) #environ为http的相关信息,如请求头等 start_response则是响应信息
return [b'<h1>Hello, web!</h1>'] #return出来是响应内容
这个application在flask里叫做wsgi_app。服务器框架在接收到http请求的时候,去调用app时,他实际上是用了Flask 的 __call__
方法。
# Flask 部分源码
class Flask(_PackageBoundObject):
def __call__(self, environ, start_response):
"""The WSGI server calls the Flask application object as the
WSGI application. This calls :meth:`wsgi_app` which can be
wrapped to applying middleware."""
return self.wsgi_app(environ, start_response)
在Flask类中又调用了 wsgi_app(environ, start_response)
方法
实际上,当http请求从server发送过来的时候,便会调用 wsgi_app
方法并传入environ和start_response。
wsgi_app
def wsgi_app(self, environ, start_response):
ctx = self.request_context(environ) # 创建请求上下文对象 RequestContext
error = None
try:
try:
ctx.push() # 入栈
response = self.full_dispatch_request()
except Exception as e:
error = e
response = self.handle_exception(e)
except:
error = sys.exc_info()[1]
raise
return response(environ, start_response)
finally:
if self.should_ignore_error(error):
error = None
ctx.auto_pop(error)
在wsgi_app
中,创建 请求上下文对象 ,并将其入栈,下面是入栈操作
class RequestContext(object):
def push(self):
...
_request_ctx_stack.push(self) # 请求上下文入栈
可以看到 ctx.push() 将 ctx 压入到 _request_ctx_stack 这个栈中,所以当我们调用 request.METHOD
时将调用 _lookup_req_object() 函数 拿到 top 。
top 此时就是上面压入的 ctx 上下文对象(RequestContext 类的实例),而 getattr(top, “request”) 将返回 ctx 实例中的 request 请求,而这个 request 就是在 ctx 的 __init__()
中根据环境变量创建的。
这下应该清楚了,每次请求在调用视图函数之前,flask 会自动把请求创建好 ctx 上下文,并存放在线程的栈中,当使用时就可以根据线程 id 拿到了所需要的变量。
总流程图
为什么要使用栈
到此,实际上已经大概清除了 request 的工作流程了,但是通常对于多线程来说,一个线程一次只会处理一个请求,也就是说当前的 request 应该是只有一个,那么为什么不是直接使用这个对象,而是要使用栈呢?
这主要是多个应用导致的,这也是 Flask 设计时的标准之一吧,也就是在一个 Python 进程中,可以拥有多个应用。由于一般都只有一个 app,栈顶存放的肯定是当前 request 对象,但是如果是多个 app,那么栈顶存放的是当前活跃的 request,也就是说使用栈是为了获取当前的活跃 request 对象。
__storage__ 的数据结构
__storage__ = {
ident1 : {
'stack' : [ctx1,ctx2,...]
},
ident2 : {
'stack' : [ctx1,ctx2,...]
},
...
}
为什么要使用 LocalProxy
可是说了这么多,为什么一定要用proxy,而不能直接调用Local或LocalStack对象呢?这主要是在有多个可供调用的对象的时候会出现问题,如下图:
我们再通过下面的代码也许可以看出一二:
# use Local object directly
from werkzeug.local import LocalStack
user_stack = LocalStack()
user_stack.push({'name': 'Bob'})
user_stack.push({'name': 'John'})
def get_user():
# do something to get User object and return it
return user_stack.pop()
# 直接调用函数获取user对象
user = get_user()
print user['name']
print user['name']
打印结果是:
John
John
再看下使用LocalProxy
# use LocalProxy
from werkzeug.local import LocalStack, LocalProxy
user_stack = LocalStack()
user_stack.push({'name': 'Bob'})
user_stack.push({'name': 'John'})
def get_user():
# do something to get User object and return it
return user_stack.pop()
# 通过LocalProxy使用user对象
user = LocalProxy(get_user)
print user['name']
print user['name']
打印结果是:
John
Bob
怎么样,看出区别了吧,直接使用LocalStack对象,user一旦赋值就无法再动态更新了,而使用Proxy,每次调用操作符(这里[]操作符
用于获取属性),都会重新获取user,从而实现了动态更新user的效果。见下图:
proxy auto select object
Flask以及Flask的插件很多时候都需要这种动态更新的效果,因此LocalProxy就会非常有用了。
结论
在 Flask 中包括了两种上下文,应用上下文(app,g)和请求上下文(request,session)。使用起来,它们可能被认为是一些全局变量,所有请求的数据均在其中。
实际上,它们都处于一个请求的局部中,对于每个请求都是独立的,通过一个特殊的数据结构来动态的设置、获取它们。