Flask核心机制之上下文源码剖析
一、前言
了解过flask的python开发者想必都知道flask中核心机制莫过于上下文管理,当然学习flask如果不了解其中的处理流程,可能在很多问题上不能得到解决,当然我在写本篇文章之前也看到了很多博文有关于对flask上下文管理的剖析都非常到位,当然为了学习flask我也把对flask上下文理解写下来供自己参考,也希望对其他人有所帮助。
二、知识储备
threadlocal
在多线程中,线程间的数据是共享的,但是每个线程想要有自己的数据该怎么实现?python中的threading.local对象已经实现,其原理是利用线程的唯一标识作为key,数据作为value来保存其自己的数据,以下是demo演示了多个线程同时修改同一变量的值的结果:
#!/usr/bin/envpython3 #-*-coding:utf-8-*- #Author:wd importthreading importtime values=threading.local() defrun(arg): values.num=arg#修改threading.local对象的name数据 time.sleep(1) print(threading.current_thread().name,values.num)#打印values.num foriinrange(3): th=threading.Thread(target=run,args=(i,),name='runthread%s'%i) th.start()
结果:
runthread00
runthread11
runthread22
结果说明:
从结果中可以看到,values.num的值是不同的,按照普通线程理解因为有sleep存在,在每个线程最后打印values.num时候值应该都是2,但是正是因为threading.local对象内部会为每个线程开辟一个内存空间,从而使得每个线程都有自己的单独数据,所以每个线程修改的是自己的数据(内部实现为字典),打印结果才不一样。
有了以上的设计思想,我们可以自己定义类似于thread.local类,为了支持协程,将其唯一标识改为协程的唯一标识,其实这已经及其接近flask中的Local类了(后续在进行说明):
try: fromgreenletimportgetcurrentasget_ident#携程唯一标识 exceptImportError: try: fromthreadimportget_ident exceptImportError: from_threadimportget_ident#线程唯一标识 classLocal(object): def__init__(self): object.__setattr__(self,'storage',dict())#防止self.xxx递归 object.__setattr__(self,'__get_ident__',get_ident) def__setattr__(self,key,value): ident=self.__get_ident__()#获取当前线程或协程的唯一标识 data=self.storage.get(ident) ifnotdata:#当前线程没有数据 data={key:value}#创建数据 else:#当前已经有数据 data[key]=value self.storage[ident]=data#最后为当前线程设置其标识对应的数据 def__getattr__(self,name): try: returnself.storage[self.__get_ident__()].get(name)#返回name所对应的值 exceptKeyError: raiseAttributeError(name)
functools.partial
partial函数是工具包的一个不常用函数,其作用是给函数传递参数,同时返回的也是这个函数,但是这个函数的已经带了参数了,示例:
fromfunctoolsimportpartial deffunc(x,y,z): print(x,y,z) new_fun=partial(func,1,2)#生成新的函数,该函数中已经有一个参数 new_fun(3)
结果:
123
在以上示例中,new_func是由func生成的,它已经参数1,2了,只需要传递3即可运行。
werkzeug
werkzeug是一个实现了wsgi协议的模块,用官方语言介绍:WerkzeugisaWSGIutilitylibraryforPython.It'swidelyusedandBSDlicensed。为什么会提到它呢,这是因为flask内部使用的wsgi模块就是werkzeug,以下是一个示例(如果你了解wsgi协议的应该不用过多介绍):
fromwerkzeug.wrappersimportRequest,Response @Request.application defapplication(request): returnResponse('HelloWorld!') if__name__=='__main__': fromwerkzeug.servingimportrun_simple run_simple('localhost',4000,application)
在示例中application是一个可调用的对象也可以是带有__call__方法的对象,在run_simple内部执行application(),也就是在源码的execute(self.server.app)中执行,这里你只需要run_simple会执行第三个参数加括号。
三、源码剖析
上下文管理
在说请求上下文之前先看一个flask的hellworld示例:
fromflaskimportFlask app=Flask(__name__) @app.route("/") defhello(): return'helloworld' if__name__=='__main__': app.run()
在以上示例中,app.run是请求的入口,而app是Flask实例化的对象,所以执行的是Flask类中的run方法,而在该改方法中又执行了run_simple方法,以下是run方法部分源码摘抄(其中self就是app对象):
fromwerkzeug.servingimportrun_simple try: run_simple(host,port,self,**options) finally: #resetthefirstrequestinformationifthedevelopmentserver #resetnormally.Thismakesitpossibletorestarttheserver #withoutreloaderandthatstufffromaninteractiveshell. self._got_first_request=False
在run_simple中会执行app(environ,start_response),参考werkzeug的源码,源码会执行app(environ,start_response)也就是执行app的__call__方法,以下是__call__方法源码摘抄:
def__call__(self,environ,start_response): """TheWSGIservercallstheFlaskapplicationobjectasthe WSGIapplication.Thiscalls:meth:`wsgi_app`whichcanbe wrappedtoapplyingmiddleware.""" returnself.wsgi_app(environ,start_response)
__call__方法中又调用了wsgi_app方法,该方法也就是flask的核心所在,下面是方法摘抄:
defwsgi_app(self,environ,start_response): """TheactualWSGIapplication.Thisisnotimplementedin :meth:`__call__`sothatmiddlewarescanbeappliedwithout losingareferencetotheappobject.Insteadofdoingthis:: app=MyMiddleware(app) It'sabetterideatodothisinstead:: app.wsgi_app=MyMiddleware(app.wsgi_app) Thenyoustillhavetheoriginalapplicationobjectaroundand cancontinuetocallmethodsonit. ..versionchanged::0.7 Teardowneventsfortherequestandappcontextsarecalled evenifanunhandlederroroccurs.Othereventsmaynotbe calleddependingonwhenanerroroccursduringdispatch. See:ref:`callbacks-and-errors`. :paramenviron:AWSGIenvironment. :paramstart_response:Acallableacceptingastatuscode, alistofheaders,andanoptionalexceptioncontextto starttheresponse. """ #ctx.app当前app名称 #ctx.requestrequest对象,由app.request_class(environ)生成 #ctx.sessionsession相关信息 ctx=self.request_context(environ) error=None try: try: ctx.push() #push数据到local,此时push的数据分请求上线文和应用上下文 #将ctx通过Localstack添加到local中 #app_ctx是APPContext对象 response=self.full_dispatch_request() exceptExceptionase: error=e response=self.handle_exception(e) except: error=sys.exc_info()[1] raise returnresponse(environ,start_response) finally: ifself.should_ignore_error(error): error=None ctx.auto_pop(error)
第一句:ctx=self.request_context(environ)调用request_context实例化RequestContext对象,以下是RequestContext类的构造方法:
def__init__(self,app,environ,request=None): self.app=app ifrequestisNone: request=app.request_class(environ) self.request=request self.url_adapter=app.create_url_adapter(self.request) self.flashes=None self.session=None
此时的request为None,所以self.request=app.request_class(environ),而在Flask类中request_class=Request,此时执行的是Request(environ),也就是实例化Request类,用于封装请求数据,最后返回RequestContext对象,此时的ctx含有以下属性ctx.app(app对象)、ctx.request(请求封装的所有请求信息)、ctx.app(当前app对象)等
第二句:ctx.push(),调用RequestContext的push方法,以下是源码摘抄:
defpush(self): """Bindstherequestcontexttothecurrentcontext.""" #Ifanexceptionoccursindebugmodeorifcontextpreservationis #activatedunderexceptionsituationsexactlyonecontextstays #onthestack.Therationaleisthatyouwanttoaccessthat #informationunderdebugsituations.Howeverifsomeoneforgetsto #popthatcontextagainwewanttomakesurethatonthenextpush #it'sinvalidated,otherwisewerunatriskthatsomethingleaks #memory.Thisisusuallyonlyaproblemintestsuitesincethis #functionalityisnotactiveinproductionenvironments. top=_request_ctx_stack.top iftopisnotNoneandtop.preserved: top.pop(top._preserved_exc) #Beforewepushtherequestcontextwehavetoensurethatthere #isanapplicationcontext. app_ctx=_app_ctx_stack.top#获取应用上线文,一开始为none ifapp_ctxisNoneorapp_ctx.app!=self.app: #创建APPContext(self)对象,app_ctx=APPContext(self) #包含app_ctx.app,当前app对象 #包含app_ctx.g,g可以看作是一个字典用来保存一个请求周期需要保存的值 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) ifhasattr(sys,'exc_clear'): sys.exc_clear() #self是RequestContext对象,其中包含了请求相关的所有数据 _request_ctx_stack.push(self) #Openthesessionatthemomentthattherequestcontextisavailable. #Thisallowsacustomopen_sessionmethodtousetherequestcontext. #Onlyopenanewsessionifthisisthefirsttimetherequestwas #pushed,otherwisestream_with_contextlosesthesession. ifself.sessionisNone: session_interface=self.app.session_interface#获取session信息 self.session=session_interface.open_session( self.app,self.request ) ifself.sessionisNone: self.session=session_interface.make_null_session(self.app)
到了这里可以看到,相关注解已经标注,flask内部将上下文分为了app_ctx(应用上下文)和_request_ctx(请求上下文),并分别用来两个LocalStack()来存放各自的数据(以下会用request_ctx说明,当然app_ctx也一样),其中app_ctx包含app、url_adapter一下是app_ctx构造方法:
def__init__(self,app): self.app=app self.url_adapter=app.create_url_adapter(None) self.g=app.app_ctx_globals_class() #Likerequestcontext,appcontextscanbepushedmultipletimes #butthereabasic"refcount"isenoughtotrackthem. self._refcnt=0
然后分别执行app_ctx.push()方法和_request_ctx_stack.push(self)方法,将数据push到stack上,_request_ctx_stack.push(self),而_request_ctx_stack是一个LocalStack对象,是一个全局对象,具体路径在flask.globals,以下是其push方法:
defpush(self,obj): """Pushesanewitemtothestack""" #找_local对象中是否有stack,没有设置rv和_local.stack都为[] rv=getattr(self._local,'stack',None) ifrvisNone: self._local.stack=rv=[] #执行Local对象的__setattr__方法,等价于a=[],rv=a,self._local.stack=a #创建字典,类似于storage={'唯一标识':{'stack':[]}} rv.append(obj) #列表中追加请求相关所有数据也就是storage={'唯一标识':{'stack':[RequestContext对象,]}} returnrv
以上代码中的self._local是一个Local()对象源码定义如下,也就是用于存储每次请求的数据,和我们刚开始定义的local及其相似,这也是为什么要先提及下threadlocal。
Local()
classLocal(object): __slots__=('__storage__','__ident_func__') def__init__(self): object.__setattr__(self,'__storage__',{}) object.__setattr__(self,'__ident_func__',get_ident) def__iter__(self): returniter(self.__storage__.items()) def__call__(self,proxy): """Createaproxyforaname.""" returnLocalProxy(self,proxy) def__release_local__(self): self.__storage__.pop(self.__ident_func__(),None) def__getattr__(self,name): try: returnself.__storage__[self.__ident_func__()][name] exceptKeyError: raiseAttributeError(name) def__setattr__(self,name,value): ident=self.__ident_func__() storage=self.__storage__ try: storage[ident][name]=value exceptKeyError: storage[ident]={name:value} def__delattr__(self,name): try: delself.__storage__[self.__ident_func__()][name] exceptKeyError: raiseAttributeError(name) Local()
到这里我们知道了,当执行ctx.push()时,local对象中已经有数据了,接着开始执行self.full_dispatch_request(),也就是开始执行视图函数,以下是源码摘抄:
deffull_dispatch_request(self): """Dispatchestherequestandontopofthatperformsrequest preandpostprocessingaswellasHTTPexceptioncatchingand errorhandling. ..versionadded::0.7 """ self.try_trigger_before_first_request_functions() try: request_started.send(self) rv=self.preprocess_request() ifrvisNone: rv=self.dispatch_request() exceptExceptionase: rv=self.handle_user_exception(e) returnself.finalize_request(rv)
在改方法中调用self.preprocess_request(),用于执行所有被before_request装饰器装饰的函数,从源码总可以看到如果该函数有返回,则不会执行self.dispatch_request()也就是视图函数,
执行完毕之后调用self.dispatch_request()根据路由匹配执行视图函数,然后响应最后调用ctx.auto_pop(error)将stack中的数据删除,此时完成一次请求。
全局对象request、g、session
在了解完flask的上下文管理时候,我们在视图函数中使用的request实际上是一个全局变量对象,当然还有g、session这里以request为例子,它是一个LocalProxy对象,以下是源码片段:
request=LocalProxy(partial(_lookup_req_object,'request'))
当我们使用request.path时候实际上是调用是其__getattr__方法即LocalProxy对象的__getattr__方法,我们先来看看LocalProxy对象实例化的参数:
def__init__(self,local,name=None): #local是传入的函数,该句等价于self.__local=local,_类名__字段强行设置私有字段值 #如果是requst则函数就是partial(_lookup_req_object,'request') object.__setattr__(self,'_LocalProxy__local',local) object.__setattr__(self,'__name__',name)#开始的时候设置__name__的值为None ifcallable(local)andnothasattr(local,'__release_local__'): #"local"isacallablethatisnotaninstanceofLocalor #LocalManager:markitasawrappedfunction. object.__setattr__(self,'__wrapped__',local)
在源码中实例化时候传递的是partial(_lookup_req_object,'request')函数作为参数,也就是self.__local=该函数,partial参数也就是我们之前提到的partial函数,作用是传递参数,此时为_lookup_req_object函数传递request参数,这个在看看其__getattr__方法:
def__getattr__(self,name): #以获取request.method为例子,此时name=method ifname=='__members__': returndir(self._get_current_object()) #self._get_current_object()返回的是ctx.request,再从ctx.request获取method(ctx.request.method) returngetattr(self._get_current_object(),name)
在以上方法中会调用self._get_current_object()方法,而_get_current_object()方法中会调用self.__local()也就是带参数request参数的_lookup_req_object方法从而返回ctx.request(请求上下文),最后通过然后反射获取name属性的值,这里我们name属性是path,如果是request.methodname属性就是method,最后我们在看看_lookup_req_object怎么获取到的ctx.request,以下是源码摘抄:
def_lookup_req_object(name): #以name=request为列 top=_request_ctx_stack.top #top是就是RequestContext(ctx)对象,里面含有request、session等 iftopisNone: raiseRuntimeError(_request_ctx_err_msg) returngetattr(top,name)#到RequestContext(ctx)中获取那么为request的值
在源码中很简单无非就是利用_request_ctx_stack(也就是LocalStack对象)的top属性返回stack中的ctx,在通过反射获取request,最后返回ctx.request。以上是整个flask的上下文核心机制,与其相似的全局对象有如下(session、g):
#contextlocals _request_ctx_stack=LocalStack()#LocalStack()包含pop、push方法以及Local对象,上下文通过该对象push和pop _app_ctx_stack=LocalStack() current_app=LocalProxy(_find_app) request=LocalProxy(partial(_lookup_req_object,'request'))#reuqest是LocalProxy的对象,设置和获取request对象中的属性通过LocalProxy定义的各种双下划线实现 session=LocalProxy(partial(_lookup_req_object,'session')) g=LocalProxy(partial(_lookup_app_object,'g'))
技巧应用
利用flask的上下文处理机制我们获取上请求信息还可以使用如下方式:
fromflaskimportFlask,_request_ctx_stack app=Flask(__name__) @app.route("/") defhello(): print(_request_ctx_stack.top.request.method)#结果GET,等价于request.method return'thisiswd' if__name__=='__main__': app.run()
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。