Flask源码之请求上下文和应用上下文
我们继续来看wsgi_app
这个方法,我去掉了一些无关代码,那些部分会在其他博文中介绍
def wsgi_app(self, environ, start_response):
# 调用了self.request_context这个方法
# 此方法把environ封装成了RequestContext对象,这个对象看名字也知道这个是请求上下文
ctx = self.request_context(environ)
def request_context(self, environ):
# 注意request_context是Flask的类方法,那么self就是Flask的实例或者说对象
# 也就是说下面的 RequestContext 的实例化需要Flask实例和environ参数
return RequestContext(self, environ)
简单看一下RequestContext
的定义,位置在源码的ctx.py
中
class RequestContext(object):
def __init__(self, app, environ, request=None, session=None):
self.app = app
if request is None:
request = app.request_class(environ)
self.request = request
我们看到, 第一个参数 app
接受的是 Flask实例
回到wsgi_app
这个方法,我们看到接下来ctx.push
这一句调用了RequestContext
的push
方法,这个方法就是把自身也就是 RequestContext
实例压入到 LocalStack
这个数据结构中
def wsgi_app(self, environ, start_response):
# ctx是 RequestContext 实例
ctx = self.request_context(environ)
error = None
try:
try:
# 只看这一句
ctx.push()
response = self.full_dispatch_request()
except Exception as e:
error = e
response = self.handle_exception(e)
except: # noqa: B001
error = sys.exc_info()[1]
raise
return response(environ, start_response)
finally:
if self.should_ignore_error(error):
error = None
ctx.auto_pop(error)
Local和LocalStack
flask
中实现了Local
和LocalStack
两种数据结构,Local
的作用是线程隔离和协程隔离,类似threadlocal,可以看成下面的字典。RequestContext
暂时可以认为存储在这样的字典中
{12345:RequestContext}
12345
是线程id,RequestContext
是请求上下文,之所以用Local
做是为了在wsgi server
是多线程或多协程的情况下,每个线程或协程获取自己的上下文,不会相互干扰。毕竟我们知道多线程是共享内存中的对象的
但实际上,flask
是把RequestContext
存储在栈(后进先出)结构中,准确地说是LocalStack
结构中,类似下面的字典
{12345:{'stack':[RequestContext]}}
这样做是因为一个请求可能有多个请求上下文,比如服务端重定向的情况(一个请求要去A
视图,我们需要重定向到B
视图,这就需要自己再push一个RequestContext
)
此外还有多个应用上下文的情况
入栈
有了对Local
和LocalStack
的说明,我们回到 wsgi_app
方法中,继续看 push方法
,看是否确实是把 RequestContext
压入到LocalStack
中
def wsgi_app(self, environ, start_response):
# ctx是 RequestContext 实例
ctx = self.request_context(environ)
error = None
try:
try:
# 只看这一句
ctx.push()
response = self.full_dispatch_request()
except Exception as e:
error = e
response = self.handle_exception(e)
except: # noqa: B001
error = sys.exc_info()[1]
raise
return response(environ, start_response)
finally:
if self.should_ignore_error(error):
error = None
ctx.auto_pop(error)
ctrl
+鼠标左键点击push
def push(self):
top = _request_ctx_stack.top
_request_ctx_stack
的定义再globals.py
中
_request_ctx_stack = LocalStack()
它现在还是的状态是这样,栈是空的
{12345:'stack':[]}
如果栈是空的话,我们把self
也就是RequestContext
压入栈,在最后一句,注意push
是RequestContext
的方法,所以self
就是RequestContext
的实例
def push(self):
top = _request_ctx_stack.top
app_ctx = _app_ctx_stack.top
if app_ctx is None or app_ctx.app != self.app:
app_ctx = self.app.app_context()
app_ctx.push()
self._implicit_app_ctx_stack.append(app_ctx)
else:
self._implicit_app_ctx_stack.append(None)
# 压入栈
_request_ctx_stack.push(self)
我们还注意到这里还有一个_app_ctx_stack
,这也是LocalStack
,位置在globals.py
_app_ctx_stack = LocalStack()
只不过这个栈里面存储的是应用上下文,类似下面的字典
{12345:'stack':[AppContext]}
然后我们也执行了app_ctx.push()
方法,也就是把应用上下文压入栈
到这里我们就知道了,执行RequestContext
对象的push方法会把RequestContext
的实例压入_request_ctx_stack
中,还会把AppContext
的实例压入_app_ctx_stack
中
如何获取请求上下文
我们常常需要在一个视图函数中获取请求中的参数,例如url
,remote_address
我们当然可以每次手动获取_request_ctx_stack
栈顶的RequestContext
对象,然后调用RequestContext
的request
属性,但每次操作栈结构还是有点繁琐,像下面这样
from flask import Flask, request, Request
from flask.globals import _request_ctx_stack
flask_app = Flask(__name__)
@flask_app.route('/')
def hello_world():
req: Request = _request_ctx_stack.top.request
remote_addr=req.remote_addr
return "{}".format(remote_addr)
flask
的做法是使用LocalProxy
LocalProxy
from flask import Flask, request
request
就是LocalProxy
,它是代理模式在flask
中的运用,作用就是代理对RequestContext.request
的操作
我们执行request.remote_addr
就相当于执行 _request_ctx_stack.top.request.remote_addr
那为什么用LocalProxy
而不是直接request=_request_ctx_stack.top.request
呢?
原因是这样写,项目run
的时候,这句 request=_request_ctx_stack.top.request
就已经执行了,但是项目启动的时候_request_ctx_stack.top
还是None
,因为还没有请求进来,push
方法还没执行。这就导致了request
固定成了None
,这显然不行
我们看看LocalProxy
是怎么做的,我先介绍原理,再介绍实现细节
原理
原理就是 request
是一个代理对象,代理了对 RequestContext.request
的操作
具体来说:
我们 重写了__getattr__
方法,让每次执行 request.remote_addr
会先去 LocalStack
中拿到 RequestContext
,然后执行 RequestContext.request.remote_addr
,获取其他属性也是一样
也就是说,代理模式延迟了 被代理对象的获取,代理对象Localproxy
创建的时候不会获取,获取被代理对象属性的时候才会获取被代理对象
实现
#LocalProxy传入了一个偏函数,偏函数的作用是获取被代理对象
request = LocalProxy(partial(_lookup_req_object, "request"))
# _lookup_req_object("request")=partial(_lookup_req_object, "request")()=RequestContext.request
def _lookup_req_object(name):
top = _request_ctx_stack.top
if top is None:
raise RuntimeError(_request_ctx_err_msg)
return getattr(top, name)
class LocalProxy(object):
__slots__ = ("__local", "__dict__", "__name__", "__wrapped__")
def __init__(self, local, name=None):
object.__setattr__(self, "_LocalProxy__local", local)
def _get_current_object(self):
if not hasattr(self.__local, "__release_local__"):
return self.__local()
def __getattr__(self, name):
return getattr(self._get_current_object(), name)
再看LocalProxy
1.__getattr__
request.remote_addr
会执行 __getattr__
这个方法,相当于__getattr__("remote_addr")
这个方法又会去调用_get_current_object
获取被代理的对象,也就是 RequestContext.request
,之后调用 getattr(self._get_current_object(), name)
获取 RequestContext.request
的指定属性,例如 remote_addr
总结
到这里,我们就可以看出flask
中的请求上下文是如何存储和获取的
请求上下文存储在LocalStack
结构中,Local
是为了线程安全,LocalStack是为了
多请求上下文的场景
而获取是通过LocalProxy
,使用代理模式是为了动态地获取请求上下文,在访问request属性的时候,才会从栈中获取真实的请求上下文,然后代理 属性的获取
flask
中还有应用上下文,curren_app
,我们有时候会通过current_app.config
来获取配置信息,原理和request
类似,只不过代理的是AppContext
对象
说了这么多,wsgi_app
的第一步,也就是请求的第一步,就是先把请求上下文和应用上下文入栈