开胃小菜
在了解flask上下文管理机制之前,先来一波必知必会的知识点。
首先,先来聊一聊面向对象中的一些特殊的双下划线方法,比如call、getattr系列、getitem系列。
call系列
这个方法相信大家并不陌生,在单例模式中,我们可能用到过,除此之外,还想就没有在什么特殊场景中用到了。我们往往忽视了它一个很特殊的用法:对象object+()或者类Foo()+()这种很特殊的用法。在Flask上下文管理中,入口就是使用了这种方式。
getitem系列
使用这个系列的方法时,我们最大的印象就是调用对象的属性可以像字典取值一样使用中括号([])。使用中括号对对象中的属性进行取值、赋值或者删除时,会自动触发对应的__getitem__、__setitem__、__delitem__方法。
class Foo(object):
def __init__(self):
self.name = "boo"
def __getitem__(self, item):
print("调用__getitem__了")
if item in self.__dict__:
return self.__dict__[item]
def __setitem__(self, key, value):
print("调用__setitem__方法了")
self.__dict__[key] = value
def __delitem__(self, key):
print("调用__delitem__")
del self.__dict__[key]
foo = Foo()
ret = foo["name"]
# print(ret) # 输出 调用__getitem__了 boo
foo["age"] = 18
# print(foo["age"]) # 输出 调用__setitem__方法了 调用__getitem__了 18
del foo["age"] # 输出 调用__delitem__
getattr系列
使用对象取值、赋值或者删除时,会默认的调用对应的__getattr__、__setattr__、__delattr__方法。
对象取值时,取值的顺序为:先从__getattribute__中找,第二步从对象的属性中找,第三步从当前类中找,第四步从父类中找,第五步从__getattr__中找,如果没有,直接抛出异常。
class Foo(object):
def __init__(self):
self.name = "boo"
def __getattr__(self, item):
print("调用__getattr__了")
def __setattr__(self, key, value):
print("调用__setattr__方法了")
def __delattr__(self, item):
print("调用__delattr__")
foo = Foo()
ret = foo.xxx # 输出 调用__getattr__了
foo.age = 18 # 调用__setattr__方法了
del foo.age # 输出 调用__delattr__
再来说说Python中的偏函数
一句话来总结partial的作用,固定函数中的一些参数,返回一个新的函数,方便调用,举个例子
from functools import partial
class Foo(object):
def __init__(self):
self.request = "request"
self.session = "session"
foo = Foo()
def func(args):
return getattr(foo,args)
re_func = partial(func,'request')
se_func = partial(func,'session')
print(re_func())
再来说一说threading.local方法
在多线程中,同一个进程中的多个线程是共享一个内存地址的,多个线程操作数据时,就会造成数据的不安全,所以我们就要加锁。但是,对于一些变量,如果仅仅只在本线程中使用,怎么办?
方法一,可以通过全局的字典,key为当前线程的线程ID,value为具体的值。
方法二,使用threading.local方法
threading.local 在多线程操作时,为每一个线程创建一个值,使得线程之间各自操作自己 的值,互不影响。
import time
import threading
local = threading.local()
def func(n):
local.val = n
time.sleep(5)
print(n)
for i in range(10):
t = threading.Thread(target=func,args=(I,))
t.start()
# 结果输出 0--9
正餐
flask的上下文管理机制
启动一个flask项目时,会先执行app.run()方法,这是整个项目的入口,执行run方法时,接着执行werkzeug模块中的run_simple
werkzeug中触发调用了Flask的call方法
请求进来时
触发执行call方法,call方法的逻辑很简单,直接执行wsgi_app方法,将包含所有请求相关数据和一个响应函数传进去。
执行wsgi_app方法
def wsgi_app(self, environ, start_response):
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:
error = sys.exc_info()[1]
raise
return response(environ, start_response)
finally:
if self.should_ignore_error(error):
error = None
ctx.auto_pop(error)
第一步先执行了一个request_context的方法,将environ传进去,最后返回一个RequestContext类的对象,被ctx的变量接收(ctx=request_context(environ))
这个ctx对象在初始化时,赋了两个非常有用的属性,一个是request,一个是session
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
这两个属性中request是一个Request()对象,这个对象就是我们在flask中使用的request对象,为我们提供了很多便捷的属性和方法,比如:request.method、request.form、request.args等等,另一个属性是session,初始为None。
紧接着执行ctx.push()方法,这个方法中,在执行请求上下文对象ctx之前先实例化了一个app_context对象,先执行了app_context的push方法,然后才执行_request_ctx_stack对象中的top和_request_ctx_stack.push(self),最后对ctx中的session进行处理。
所以,flask中的应用上下文发生在请求上下文之前。
在ctx.push方法的执行逻辑
def push(self):
top = _request_ctx_stack.top
if top is not None and top.preserved:
top.pop(top._preserved_exc)
# 在执行request_context请求上下文的push方法时,先执行了app_context应用上下文的push方法
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)
if hasattr(sys, 'exc_clear'):
sys.exc_clear()
# 然后执行请求上下文对象中LocalStack对象的push方法
_request_ctx_stack.push(self)
# 最后处理session
if self.session is None:
session_interface = self.app.session_interface
self.session = session_interface.open_session(
self.app, self.request
)
if self.session is None:
self.session = session_interface.make_null_session(self.app)
但是我们先说请求上下文,在处理完应用上下文的push方法后,紧接着执行了_request_ctx_stack对象的两个方法。
而这个_request_ctx_stack是LocalStack这个类的对象。_request_ctx_stack = LocalStack()
如果对象中没有某个属性,取值时,最终会执行类中的getattr方法,然后再做后续的异常处理,flask将所有的对应逻辑都实现在了类的getattr方法中,将每一个线程存储到字典中,在请求进来时,将每一个对应的请求ctx存在一个列表中,使用时直接调用,而不是通过传参的形式,更体现出了flask框架的轻量级。
处理完_request_ctx_stack后,就该处理session了。
在flask中,处理session时,非常的巧妙,完美的遵循了开闭原则,会先执行session_interface对象的open_session方法,在这个方法中,会先从用户请求的cookie中获取sessionid,获取该用户之前设置的session值,然后将值赋值到ctx.session中。
处理完session后,ctx.push方法就执行完了,返回到最开始的app.wsgi_app方法中,执行完push方法后,接着执行full_dispatch_request方法,从这个名字中我们也能猜到,这个方法只要是负责请求的分发。
执行视图函数时
在执行视图函数之前,先执行了before_request,在执行我们的视图函数。
视图函数主要处理业务逻辑。在视图函数中可以调用request对象,进行取值,也可以调用session对象对session的存取。
在整个request的请求生命周期中,获取请求的数据直接调用request即可,对session进行操作直接调用session即可。request和session都是LocalProxy对象,借助偏函数的概念将对应的值传入_lookup_req_object函数。先从_request_ctx_stack(LocalStack)对象中获取ctx(请求上下文对象),再通过反射分别获取request和session属性。整个过程中LocalStack扮演了一个全局仓库的角色,请求进来将数据存取,需要时即去即用。所以,flask实现了在整个请求的生命周期中哪儿需要就直接调用的特色。
request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))
请求结束
视图函数执行完后,dispatch_request执行结束,执行full_dispatch_request方法的返回值finalize_request方法。这个方法中,同样的,在返回响应之前,先执行所有被after_request装饰器装饰的函数。
def process_response(self, response):
ctx = _request_ctx_stack.top
bp = ctx.request.blueprint
funcs = ctx._after_request_functions
if bp is not None and bp in self.after_request_funcs:
funcs = chain(funcs, reversed(self.after_request_funcs[bp]))
if None in self.after_request_funcs:
funcs = chain(funcs, reversed(self.after_request_funcs[None]))
for handler in funcs:
response = handler(response)
if not self.session_interface.is_null_session(ctx.session):
self.session_interface.save_session(self, ctx.session, response)
return response
执行process_response过程中,执行完after_request后,然后,执行session的save_session方法。将内存中保存在ctx.session的值取到后,json.dumps()序列化后,写入响应的cookie中(set_cookie),最后返回响应。
返回响应后,自动的调用ctx.auto_pop(error),将Local中存储的ctx对象pop掉,整个请求结束。
甜点
如果感觉上面的源码分析太难以理解,可以根据下面的请求上下文的执行流程图来加深印象
请求上下文的执行流程: