flask源码解析
本篇主要是针对于以下一些问题进行源码剖析,并补充解释一些python语法的用法与应用场景。
- flask生命周期流程
- flask的request、session等都是导入进来的,也就意味着每次请求,我们所用的都是同一个request对象,它为什么能够按照同种方式取到自己request对象值呢
flask生命执行流程
app.run()
启动项目,使用app.run,app是Flask对象:
def run(self,host,post,debug,...):
# 加载一些配置,启动服务
...
# 用werkzeug开启web服务
from werkzeug.serving import run_simple
run_simple(t.cast(str, host), port, self, **options)
开启web服务
需要借助符合wsgi协议的web服务器能够开启web服务,django使用的是wsgiref,而flask用的是werkzeug,那么代码上如何实现wsgi服务呢。
wsgiref开启web
from wsgiref.simple_server import make_server
# mya 就等同于django
def mya(environ, start_response):
#把environ包装成了request
print(environ)
start_response('200 OK', [('Content-Type', 'text/html')])
if environ.get('PATH_INFO') == '/index':
with open('index.html','rb') as f:
data=f.read()
elif environ.get('PATH_INFO') == '/login':
with open('login.html', 'rb') as f:
data = f.read()
else:
data=b'<h1>Hello, web!</h1>'
return [data] # 做成了response
if __name__ == '__main__':
myserver = make_server('', 8008, mya)
print('监听8010')
myserver.serve_forever()
werkzeug开启web
from werkzeug.wrappers import Request, Response
@Request.application
def hello(request):
return Response('Hello World!')
if __name__ == '__main__':
from werkzeug.serving import run_simple
run_simple('localhost', 4000, hello)
将函数用Request.application装饰为视图函数,并返回Response对象,那么run_simple函数中传入这个函数hello,并监听指定端口,每次http请求来时,就相当于执行一次函数hello,因为hello函数返回了Response对象,所以可以被顺利的处理成http响应返回给网页。
run_simple
所以了解了run_simple()的运行机制,那么我们就可以解释app.run中的run_simple(t.cast(str, host), port, self, **options)
。
因为self是app本身,所以run_simple监听http请求后会执行的是app()
,对象调用时会触发__call__
方法,它之中执行了return self.wsgi_app(environ, start_response)
根据我们刚才对werkzeug开启web的解释,这里一定会返回一个Response对象。而在wsgi_app内部是关于flask框架为我们做了一些功能封装。
app.wsgi_app
def wsgi_app(self, environ: dict, start_response: t.Callable) -> t.Any:
ctx = self.request_context(environ)
error: t.Optional[BaseException] = 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 "werkzeug.debug.preserve_context" in environ:
environ["werkzeug.debug.preserve_context"](_cv_app.get())
environ["werkzeug.debug.preserve_context"](_cv_request.get())
if error is not None and self.should_ignore_error(error):
error = None
ctx.pop(error)
不难发现,这段代码的核心步骤为:
ctx = self.request_context(environ)
ctx.push()
response = self.full_dispatch_request()
return response(environ, start_response)
ctx为context的缩写,表示上下文,这其实是为什么不同请求处理中能用同一个request的同样方式还能准确使用自己数据的关键。
self.request_context(environ)
会返回一个RequestContext(self, environ)
对象赋值给ctx。
这个ctx是根据本次请求的environ字典(werkzurg帮我们将请求处理成了字典)生成的对象,其初始化时就将请求对象做了很多的处理。将本次请求的request、session、flash等放在了这个RequestContext对象ctx中。
ctx.push是RequestContext的push方法,它内部会执行_request_ctx_stack.push(self)
,self是ctx本身,_request_ctx_stack
是LocalStack对象(程序启动后就产生的一个单例对象),它执行了push并传入了ctx(这个ctx是本次请求特有的)
## LocalStack
def push(self, obj):
# self._local是 Flask自己定义的兼容线程和协程的LocalProxy对象
# self._local中反射 stack,会根据不同线程或协程,返回不同线程的stack
#rv是None的
rv = getattr(self._local, "stack", None)
if rv is None:
# rv=[]
# self._local.stack=rv
#self._local={'协程id号1':{stack:[]},'协程id号2':{stack:[]}}
self._local.stack = rv = []
rv.append(obj)
#self._local={'协程id号1':{stack:[ctx,]},'协程id号2':{stack:[]}}
return rv
self._local引用了全局变量代理local,程序启动时就产生了,在local中有塞入stack,会根据线程或者协程号将obj(传入的ctx)存放起来。
Local与LocalProxy
我们在处理多线程数据时,需要注意同一进程下的多个线程的全局变量是共享的。我们在处理数据通常采取加锁处理,来避免数据错乱。
不过有这么一个应用场景,每个线程的任务想要使用的变量来自于这个线程其他任务函数,有时这些函数是跨文件的,我们需要把一些数据导入到全局使用。那我们又想多个线程间的数据不错乱,就需要用到threading.local对象,如果上面这段话有点拗口难懂,看下面的代码即可:
from threding import Thread, Local
g = Local()
def task(arg):
g.value = arg
time.sleep(1)
print g.value
for i in range(10):
t = Thread(target=task, args=(i, ))
t.start()
"""执行结果,是打印出了0~9的数字而不是全是一个值"""
上面程序的执行结果说明了,每个线程虽然都是在1秒前左右赋值全局的g.value,在1秒后再打印全局g.value,但是在打印时,打印的都是自己线程中所赋值的数据,并没有出现数据错乱。
那么这个g对象,我在一个文件中使用Local产生,导入到一个文件中进行赋值,再导入另一个文件中进行取值,这样的操作也就可以实现了,这就符合我们对上下文操作的要求了。
实际上,flask中的全局request,session,g也就是这样的原理。
但是flask自己写了一个LocalProxy类,因为原生的Local是根据线程号来区分不同任务的数据的,但flask还可以开启协程去处理任务,所以为了兼容协程,flask自己封装了一个LocalProxy,即使任务是不同协程开启的,这个全局变量LocalProxy对象也能区分开。
- Local是内置模块threading提供的全局变量代理,由线程号区分任务的数据
- LocalProxy是flask封装的全局变量代理,由协程号(如果有协程模块的话)区分任务的数据
了解了以上内容后,再回到self._local.stack=rv=[]
和rv.append(obj)
,再理解就方便了,就是将我们之前处理好的ctx内容存入了local.stack中,并且按照线程号/协程号可以找到它。
至此,从run执行到视图之前,我们关于request,session,g等全局变量的存储处理就有了个大概的认识,它们都被打包到了ctx的上下文变量中,并放入了local全局变量代理中,下文则要说明取出这些数据使用的源码是如何的对应的
request.method
-
request = LocalProxy(partial(_lookup_req_object, "request"))
request实际上是通过了以上代码先产生了一个全局变量代理对象。其中partial(_lookup_req_object, "request")是偏函数,我们先传入了参数,当后面再加括号调用时才真正执行。
-
request取属性会触发LocalProxy的
__getattr__
,传入name比如这里的'method',返回getattr(self._get_current_object(), name)
,也就是由self._get_current_object()
反射出的method -
_get_current_object()
:def _get_current_object(self): if not hasattr(self.__local, "__release_local__"): return self.__local() # self中的 __local,隐藏属性 try: return getattr(self.__local, self.__name__) except AttributeError: raise RuntimeError("no object bound to %s" % self.__name__)
__local
是LocalProxy的隐藏属性,在__init__
中赋的值 -
__init__
def __init__(self, local, ...): object.__setattr__(self, "_LocalProxy__local", local)
只看这一部分我们即可得知,local是生成对象时传入的,它就是
request = LocalProxy(partial(_lookup_req_object, "request"))
中的partial(_lookup_req_object, "request")
,所以在第三步_get_current_object()
中我们通过self.__local()
去执行了_lookup_req_object('request')
函数,并作为返回值 -
_lookup_req_object('request')
def _lookup_req_object(name): # 这里把当前线程下 的ctx取出来了 top = _request_ctx_stack.top if top is None: raise RuntimeError(_request_ctx_err_msg) return getattr(top, name) # 去ctx中反射request,返回的就是当次请求的request
所以这个当次请求的request就是第三步
_get_current_object()
的结果,第二步request.method触发的__getattr__
返回的值就是getattr(当次请求的request,'method')
整理完上述取值过程,对于所有request,session,g等本地代理的取值实际上是类似的,都是先传了个偏函数作为self.__local
,在_get_current_object()
判断如果是偏函数就加括号调用,那么调用后就得到了当前线程的相应的对象,我们在调用有关于这个函数有关属性和方法时都经过代理的__getattrr__
反射出了原本对象该有的属性。