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'])
    
  • 相关阅读:
    Head first javascript(七)
    Python Fundamental for Django
    Head first javascript(六)
    Head first javascript(五)
    Head first javascript(四)
    Head first javascript(三)
    Head first javascript(二)
    Head first javascript(一)
    Sicily 1090. Highways 解题报告
    Python GUI programming(tkinter)
  • 原文地址:https://www.cnblogs.com/xinsiwei18/p/9551397.html
Copyright © 2011-2022 走看看