zoukankan      html  css  js  c++  java
  • flask源码剖析--请求流程

      想了解这篇里面的内容,请先去了解我另外一篇博客Flask上下文  

      在了解flask之前,我们需要了解两个小知识点

    • 偏函数
    import functools
    
    def func(a1,a2):
        print(a1)
        print(a2)
    
    #重新封装成一个 给前面参数加默认值 的函数
    new_func = functools.partial(func, 666)
    new_func(777)
    
    •  面向对象   对象 + 会执行__add__方法
    class Foo(object):
    
        def __init__(self, num):
            self.num = num
    
        def __add__(self, other):
            data = self.num + other.num
            return Foo(data)
    
    obj1 = Foo(11)
    obj2 = Foo(22)
    v = obj1 + obj2
    print(v.num)
    
    •  拼接列表中的值
    from itertools import chain
    
    v1 = [11,22,33]
    v2 = [44,55,66]
    
    new = chain(v1, v2)
    for item in new:
        print(item)
    
    
    def f1(x):
        return x + 1
    
    func1_list = [f1, lambda x:x-1]
    
    def f2(x):
        return x + 10
    
    new_func_list = chain([f2], func1_list)
    for func in new_func_list:
        print(func)
    
    • 强制调用私有变量 _类名__私有变量名
    class Foo(object):
    
        def __init__(self):
            self.name = 'alex'
            self.__age = 18
    
        def get_age(self):
            return self.__age
    
    obj = Foo()
    # print(obj.name)
    # print(obj.get_age())
    # 强制获取私有字段
    print(obj._Foo__age)
    

      为什么要了解这个几个点呢?这边先按下不说,分析过程中,自然就明白了

     

      从哪里开始,那就要留心我们平时写代码了,flask程序运行起来主要app.run实现的

      进入run函数中,代码最终执行了run_simple(host, port, self, **options),而run_simple执行时,第三参数加括号执行,也就是会执行self(flask对象)__calll__方法

      __call__方法里,执行了self.wsgi_app(environ, start_response),而这里面的代码可以说是flask处理请求的核心代码

            ctx = self.request_context(environ)
            ctx.push()
            error = None
            try:
                try:
                    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)
    

    请求到来时

      第一步

      ctx = self.request_context(environ) 函数里执行了RequestContext(self, environ)

    • 执行RequestContext类里的__init__方法
        def __init__(self, app, environ, request=None):
            self.app = app  #app为flask对象
            if request is None:
                request = app.request_class(environ)
            self.request = request  #请求相关
            self.url_adapter = app.create_url_adapter(self.request)  #url映射
            self.flashes = None  #闪现相关
            self.session = None  #session相关
    
    1. 注意此时self为RequestContext对象,也就是赋值给ctx变量的对象
    2. __init__方法主要把flask对象,生成请求对象并把请求和闪现,session...相关的东西封装在ctx里

      第二步

    • ctx.push() 执行RequestContext里的push方法,函数里最终执行了_request_ctx_stack.push(self)
    • 执行的是LocalStack的push方法,并把RequestContext(ctx)对象为参数传了进来
            rv = getattr(self._local, 'stack', None)
            if rv is None:
                self._local.stack = rv = []
            rv.append(obj)
            return r
    
    1.  刚开始进入stack是没有值的,所以会执行self._local.stack = rv = [],等同把空列表同时赋值给stack和rv
    2. self._local.stack = [],本质上会执行self._local对象里的__setattr__方法,也就是Local类的
       #其中name为stack,value为[]
        def __setattr__(self, name, value):
            ident = self.__ident_func__()  #线程或协程唯一标识
            storage = self.__storage__  #__init__方法赋值为{}
            try:
                storage[ident][name] = value
            except KeyError:
                storage[ident] = {name: value}
    

       3. 等同把RequestContext对象放到Local的这么一个字典中

    storage = {
      唯一ID:{
           stack:[RequestContext对象,] 
        },
      唯一ID:{
           stack:[RequestContext对象,] 
        }    
    }
    

    请求结束时

    • 在wsgi_app函数中,在try代码中无非就是url映射,找到视图函数并执行,执行完后就需要到存储字典里把相关request数据移除掉,而在finally最终会执行这么一句代码ctx.auto_pop(error)--error有错误时就是错误信息没有就是None,也就是执行RequestContext类里的auto_pop方法
    • else中执行了self.pop(exc)  self为RequestContext对象,pop里最终执行_request_ctx_stack.pop(),而_request_ctx_stack则是LocalStack对象,也就是执行了LocalStack类的pop方法
            stack = getattr(self._local, 'stack', None)  #stack 类似这么个列表[RequestContext对象,]
            if stack is None:
                return None
            elif len(stack) == 1:
                release_local(self._local)
                return stack[-1]
            else:
                return stack.pop()  #列表pop进行删除
    

    请求处理中

    • 找到视图函数并执行请求
    1. print(request),打印对象,会执行类里的__str__方法,那这个request是哪个类的呢,要知道这个,就需要知道在导入时,是从哪导的
    from flask import Flask,request
    
    •  也就是执行了下面这句代码,也许你会觉得这个partial怎么似曾相识啊,这个就是偏函数,把request字符串当默认值传入,并且还是返回函数,所以这句代码就是实例化LocalProxy对象,并把一个偏函数传了进去,此时会去执行LocalProxy的__init__,所以说在导入的request对象,本质是LocalProxy对象
    request = LocalProxy(partial(_lookup_req_object, 'request'))
    
    • 执行LocalProxy的__init__方法时,有这么一句代码,其中_LocalProxy__local是不是感觉有在哪见过,对没错,这就是强制调用私有变量,本质上就是做了__local = 偏函数,local是传进来的偏函数
    object.__setattr__(self, '_LocalProxy__local', local)
    
    •  回到视图函数进行打印request则会执行LocalProxy里的__str__方法,也就是下面这句
        __str__ = lambda x: str(x._get_current_object())
    
    • _get_current_object函数里,进入if中执行__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__)
    
    •  而在_get_current_object主要做了这么一件事,提取偏函数并执行,也就是执行下面这个函数
    def _lookup_req_object(name):  #此时的name为request字符串
        top = _request_ctx_stack.top   
        if top is None:
            raise RuntimeError(_request_ctx_err_msg)
        return getattr(top, name)
    
    • _request_ctx_stack.top,执行LocalStack的top,最终返回就是字典存储的RequestContext对象
            try:
                return self._local.stack[-1]
            except (AttributeError, IndexError):
                return None
    
    •  所以最终getattr(top, name),是去RequestContext中获取request数据的
    • request.method,执行LocalProxy的__getattr__方法
    • 还是执行_get_current_object方法,获取偏函数_lookup_req_object执行,提取request数据

    上下文总结:

      threading.Local和Flask自定义Local对象

      --请求到来

        - ctx = 封装RequestContext(request, session)

        - ctx放到Local中

      --执行视图时

        - 导入request

        - print(request)      -->   LocalProxy对象的__str__

        - request.method   -->   LocalProxy对象的__getattr__
                   - request + 1          -->  LocalProxy对象的__add__

          - 调用 _lookup_req_object函数:去local中将requestContext想获取到,再去requestContext中获取request或session

      -- 请求结束

        - ctx.auto_pop()

        - ctx从local中移除

     

      了解整个请求流程源码后,其实你也可以这么做

    from flask.globals import _request_ctx_stack
    from functools import partial
    
    def _lookup_req_object(name):
        # name = request
        # top= ctx
        top = _request_ctx_stack.top
        if top is None:
            raise RuntimeError('不存在')
        # return ctx.request
        return getattr(top, name)
    
    class Foo(object):
        def __init__(self):
            self.xxx = 123
            self.ooo = 888
    
    req = partial(_lookup_req_object,'xxx')
    xxx = partial(_lookup_req_object,'ooo')
    
    # 当前求刚进来时
    _request_ctx_stack.push(Foo())
    
    # 使用
    # obj = _request_ctx_stack.top
    # obj.xxx
    v1 = req()
    print(v1)
    v2 = xxx()
    print(v2)
    
    
    # 请求终止,将local中的值移除
    _request_ctx_stack.pop()
    

    APP上下文

      由于flask版本的区别,有一部分数据被分离到APP上下文中,而其原理和请求上下文是一样的

    • 我们再次看到app.wsgi_app源码中这句ctx.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是RequestContext对象,即ctx,self.app就是封装在里面的flask对象,再看下app_context方法中干了啥
    • 返回的是一个AppContext,并把flask对象传入了进去,并执行AppContext的__init__方法,封装了flask对象给app,还封装了一个g对象,对于这个对象,你就可以理解为一个存储数据的字典
        def __init__(self, app):
            self.app = app
            self.url_adapter = app.create_url_adapter(None)
            self.g = app.app_ctx_globals_class()
    

       为了方便你理解这个g中主要存储什么数据,这里不妨举个例子:比如你在请求前有个数据要传递给视图函数使用,你会怎么搞了,你可能会想,直接通过request.xxx=yyy赋值就可以了,这样做是可以的,但是为了更好避免名字重了,你用这个g来进行存储,这里需要注意的是:它主要保存的是一个请求周期的值

    from flask import Flask,request,g
    
    app = Flask(__name__)
    
    @app.before_request
    def before():
        g.permission_code_list = ['list','add']
    
    
    @app.route('/',methods=['GET',"POST"])
    def index():
        print(g.permission_code_list)
        return "index"
    
    
    if __name__ == '__main__':
        app.run()
    
    •  app_ctx.push()-->_app_ctx_stack.push(self),其中_app_ctx_stack又是一个LocalStack对象,执行它里面的push,会把app_ctx(AppContext对象)放到_app_ctx_stack下的local,这里会注意到:无论多少个线程,会创建请求上下文和APP上下文两个local对象进行存储数据
    • 上面是请求到来时,请求结束时,也会进行删除,和请求上下文是一样,可以通过ctx.auto_pop(error)一步一步看下去,最终会执行_app_ctx_stack.pop(),也就是LocalStack的pop
    • 请求过程中,比如打印g对象,还是执行LocalProxy的__str__方法,最终还是执行偏函数,不过这里偏函数换成了_lookup_app_object,也是去local中获取到AppContext对象,并获取到g对象

    多APP应用

      我们已经学过了蓝图,url过来,经过app分发给蓝图进行处理

      接下来的内容,则可以通过不同的APP来处理不同的url

    from werkzeug.wsgi import DispatcherMiddleware
    from werkzeug.serving import run_simple
    from flask import Flask, current_app
    
    app1 = Flask('app01')
    
    app2 = Flask('app02')
    
    
    
    @app1.route('/index')
    def index():
        return "app01"
    
    
    @app2.route('/index2')
    def index2():
        return "app2"
    
    # http://www.oldboyedu.com/index
    # http://www.oldboyedu.com/sec/index2
    dm = DispatcherMiddleware(app1, {
        '/sec': app2,
    })
    
    if __name__ == "__main__":
        app2.__call__
        run_simple('localhost', 5000, dm)
    

       对上面这段代码的实现原理,看了源码后,你才会发现,原来如此简单

    • 先看到run_simple,最终会执行第三参数+(),也就是dm()
    • dm是一个DispatcherMiddleware对象,里面封装了起始app以及其他app和url的映射关系
        def __init__(self, app, mounts=None):
            self.app = app
            self.mounts = mounts or {}
    
    •  dm()会执行DispatcherMiddleware的__call__方法
        def __call__(self, environ, start_response):
            script = environ.get('PATH_INFO', '')  #获取url信息,比如/sec/index2
            path_info = ''
            while '/' in script:
                if script in self.mounts:  #判断当前url在不在映射关系里
                    app = self.mounts[script]  #在就获取url对应的app对象,退出循环
                    break
            #分割后 script='/sec' last_itme='index2' script, last_item = script.rsplit('/', 1) #如果不在映射关系里,从右进行分割一次 path_info = '/%s%s' % (last_item, path_info) #path_info="/index2" 用于去APP下面找视图函数 else: app = self.mounts.get(script, self.app) #如果都没有匹配到,那就获取 起始app #最后把处理好的路径信息重新封装到environ中
         original_script_name = environ.get('SCRIPT_NAME', '') environ['SCRIPT_NAME'] = original_script_name + script environ['PATH_INFO'] = path_info return app(environ, start_response)
    •  源码最后执行了app(environ, start_response),如果是/sec/index2,那么此时的app就是app2,是一个flask对象,加括号,会执行里面的__call__,从这里开始又和单app情况下执行流程是一样的

      最后,多app情况下,local下存储结构是咋样的呢?

    • 和单app的场景是一样的,同时通过线程和协程唯一标识进行存储的
    1. 在local下获取值和设置值的,不存在通过类似app唯一标识进行操作,只通过线程和协程唯一标识
    2. 在上述代码中,如果导入request,两个app是暴露在同一全局变量下的,request不对app进行区分
        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  #name  -->'stack'
            except KeyError:
                storage[ident] = {name: value}
    

      可能大家一直有个疑问,那就是local存储中存储APPcontent对象为什么要用栈呢?

        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
    

       正常请求流程中,是不会出现栈里有两个对象的,但是测试在进行离线脚本测试时,就有可能有两个呢?

      想了解清楚,这里还是了解一下面向对象的一个知识点

    class SQLHelper(object):
    
        def open(self):
            pass
    
        def fetch(self,sql):
            pass
    
        def close(self):
            pass
    
        def __enter__(self):
            self.open()
            return self
    
        def __exit__(self, exc_type, exc_val, exc_tb):
            self.close()
    
    
    # obj = SQLHelper()
    # obj.open()
    # obj.fetch('select ....')
    # obj.close()
    
    
    with SQLHelper() as obj: # 自动调用类中的__enter__方法, obj就是__enter__返回值
        obj.fetch('xxxx')
        # 当执行完毕后,自动调用类 __exit__ 方法
    

       当测试脚本中,存在with嵌套时,栈中就可能有多个对象

    from flask import Flask,current_app,globals,_app_ctx_stack
    
    app1 = Flask('app01')
    app1.debug = False # 用户/密码/邮箱
    # app_ctx = AppContext(self):
    # app_ctx.app
    # app_ctx.g
    
    app2 = Flask('app02')
    app2.debug = True # 用户/密码/邮箱
    # app_ctx = AppContext(self):
    # app_ctx.app
    # app_ctx.g
    
    
    
    with app1.app_context():# __enter__方法 -> push -> app_ctx添加到_app_ctx_stack.local
        # {<greenlet.greenlet object at 0x00000000036E2340>: {'stack': [<flask.ctx.AppContext object at 0x00000000037CA438>]}}
        print(_app_ctx_stack._local.__storage__)
        print(current_app.config['DEBUG'])
    
        with app2.app_context():
            # {<greenlet.greenlet object at 0x00000000036E2340>: {'stack': [<flask.ctx.AppContext object at 0x00000000037CA438> ]}}
            print(_app_ctx_stack._local.__storage__)
            print(current_app.config['DEBUG'])
    
    	#退出时,执行__exit__方法,执行pop,移除栈里最后的一个对象
        print(current_app.config['DEBUG'])
    
  • 相关阅读:
    一周之内了解一个行业的方法
    du命令 实现Linux 某个文件夹下的文件按大小排序
    蝴蝶效应、青蛙现象、鳄鱼法则、鲇鱼效应.......
    MYSQ提高L查询效率的策略总结
    12个高矮不同的人,排成两排(catalan数)
    四人过桥、三盏灯 三个开关 的答案
    给定一个存放整数的数组,重新排列数组使得数组左边为奇数,右边为偶数。
    一个int 数组,里面数据无任何限制,要求求出所有这样的数a[i],其左边的数都小于等于它,右边的数都大于等于它。能否只用一个额外数组和少量其它空间实现。
    二分检索(查找)的各种变种
    排序算法稳定性
  • 原文地址:https://www.cnblogs.com/xinsiwei18/p/9551397.html
Copyright © 2011-2022 走看看