Flask源码之请求上下文和应用上下文(三)

原理

Request

Flask把前端传过来的数据environ封装成了 flask.wrappers.Request

这个类的实例又是RequestContextrequest属性值

class RequestContext(object):
    request=flask.wrappers.Request(environ)

当然实际的代码是这样

class RequestContext(object):
    request=app.request_class(environ)

这个app.request_class= flask.wrappers.Request

之所以这样写,是为了扩展性,你可以修改Flaskrequest_class属性来自定义你的Request

from flask import g, request
    
def get_tenant_from_request():
    auth = validate_auth(request.headers.get('Authorization'))
    return Tenant.query.get(auth.tenant_id)
        
def get_current_tenant():
    rv = getattr(g, 'current_tenant', None)
    if rv is = None:
        rv = get_tenant_from_request()
        g.current_tenant = rv
    return rv

例如

import flask
class MyFlask(flask.Flask):
    request_class=MyRequest
    
class MyRequest(flask.wrappers.Request):
    pass

继续来看RequestContext,这个类在源码中实例化了

ctx=RequestContext(self,envirion)

所以,也就是说以后我们只要拿到这个实例ctx,然后访问ctx.request就相当于访问flask.wrappers.Request了,也就相当于可以访问envirion,,而RequestContext就是请求上下文。

ctx存储在哪里呢?怎么访问呢?

RequestContext

存储

实际上,ctx存在栈结构中,也就是后进先出,这是为了处理一个客户端请求需要多个ctx的情况

用伪代码表示就是

stack=[RequestContext(),RequestContext()]

而且,我们知道有的wsgi server对于每个请求都开一个线程,因此为了处理多线程隔离的情况,这个栈结构又存在了local中,这个local数据结构类似ThreadLocal,他们共同组成了LocalStack

用伪代码表示就是

localstack={0:{"stack":stack}} # 0是线程id或者协程id

访问

访问ctx.request也不是直接访问的,是通过一个代理类,叫LocalProxy,他是代理模式在flask中的应用

具体来说你要访问 ctx.request 的某个属性,先访问LocalProxy的对应属性,LocalProxy帮你访问

LocalProxy代理了对 ctx.request的所有操作

伪代码就是

# 例如flask.wrapper.Request()有一个get_json()方法
# localproxy也实现这个方法,帮我们访问
class LocalProxy(object):
    def get_json(self):
        # 从localstack中获取请求上下文
        ctx:RequestContext=local["这次请求的线程id"]["stack"].top
        json_data=ctx.request.get_json()
        return json_data

当然这个例子是不真实的,如果对于每个 flask.wrapper.Request的方法我们都在 LocalProxy实现一遍,那太麻烦了

request

我们经常会引用这个request对象,它实际上就是LocalProxy的实例,位置在flask.globals.py

from flask import request
request = LocalProxy(partial(_lookup_req_object, "request"))

他代理了对 RequestContext.request也就是flask.wrappers.Request实例的操作

current_appg则分别代理了对AppContext.app(也就是flask实例)和 AppContext.g的操作

实现

Stack

一个栈结构,一般要实现 push,top,pop这几个方法

class Stack(object):
    def __init__(self):
        self.items = []

    def push(self, item):
        self.items.append(item)

    def is_empty(self):
        return self.items == []

    def pop(self):
        if self.is_empty():
            return None
        # 后进先出
        return self.items[-1]
![image-20210123001932225](https://upload-images.jianshu.io/upload_images/9003674-0a0db7f8b46d9d24.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

Local

local源码,作用就是线程或者协程隔离

class Local(object):
    __slots__ = ("__storage__", "__ident_func__")

    def __init__(self):
        # 实例化的时候给self绑定__storage__属性,初始值是{}
        object.__setattr__(self, "__storage__", {})
        # 实例化的时候给self绑__ident_func__属性,初始值是get_ident函数,这个方法用于获取当前线程或协程id
        object.__setattr__(self, "__ident_func__", get_ident)
        
    def __setattr__(self, name, value):
        # 这个方法会在属性被设置时调用
        # 因此如果我们这样操作
        # s=Stack()
        # s.a=1
        # 那么self.__storage__属性就变成了 {0:{"a",1}},0表示当前线程id
        ident = self.__ident_func__()
        storage = self.__storage__
        try:
            storage[ident][name] = value
        except KeyError:
            storage[ident] = {name: value}
            
    def __getattr__(self, name):
        # 获取属性时,该方法会被调用
        # 容易看出,通过把线程或协程id设置为key,可以实现线程或写成隔离
        try:
            return self.__storage__[self.__ident_func__()][name]
        except KeyError:
            raise AttributeError(name)

    def __iter__(self):
        # 迭代的时候会被调用
        return iter(self.__storage__.items())

    def __release_local__(self):
        # 清空当前线程的堆栈数据
        self.__storage__.pop(self.__ident_func__(), None)

    def __delattr__(self, name):
        # 删除属性时候会被调用
        try:
            del self.__storage__[self.__ident_func__()][name]
        except KeyError:
            raise AttributeError(name)

LocalStack

LocalStack大概相当于

{0:{"stack":[ctx,]}}
class LocalStack(object):

    def __init__(self):
        # _local属性是Local对象,相当于字典{}
        # push方法就是给_local实例添加一个stack属性,初始值是[],然后append
        # {0:'stack':[]}
        self._local = Local()

    def __release_local__(self):
        self._local.__release_local__()

    @property
    def __ident_func__(self):
        return self._local.__ident_func__

    @__ident_func__.setter
    def __ident_func__(self, value):
        object.__setattr__(self._local, "__ident_func__", value)

    def push(self, obj):
        rv = getattr(self._local, "stack", None)
        if rv is None:
            self._local.stack = rv = []
        rv.append(obj)
        return rv

    def pop(self):
        stack = getattr(self._local, "stack", None)
        if stack is None:
            return None
        elif len(stack) == 1:
            release_local(self._local)
            return stack[-1]
        else:
            return stack.pop()

    @property
    def top(self):
        try:
            return self._local.stack[-1]
        except (AttributeError, IndexError):
            return None
        
    def __call__(self):
        def _lookup():
            rv = self.top
            if rv is None:
                raise RuntimeError("object unbound")
            return rv

        return LocalProxy(_lookup)

值得注意的是,当我们执行如下代码

ls=LocalStack()
ls()

会调用 __call__方法,返回的是目前栈顶对象的代理对象,栈顶对象例如RequestContext

LocalProxy

LocalProxy就是代理对象了,它可以代理对RequestContext的操作

class LocalProxy(object):
    __slots__ = ("__local", "__dict__", "__name__", "__wrapped__")
    def __init__(self, local, name=None):     
        object.__setattr__(self, "_LocalProxy__local", local)


    def _get_current_object(self):
        if not hasattr(self.__local, "__release_local__"):
            return self.__local()

    def __getattr__(self, name):
        return getattr(self._get_current_object(), name)

原理很简单,我们访问 LocalProxy的某个属性,会调用 __getattr__方法,__getattr__方法又会调用 _get_current_object去获取栈顶对象或者栈顶对象的属性,例如获取RequestContext对象或者RequestContext.request

request对象为例

他是一个LocalProxy的实例

request = LocalProxy(partial(_lookup_req_object, "request"))

LocalProxy实例化传入了一个偏函数 _lookup_req_object(偏函数作用就是固定函数的参数),这个函数的作用就是获取栈顶的RequestContext对象的request属性,也就是 flask.wrappers.Request()

def _lookup_req_object(name):
    top = _request_ctx_stack.top
    if top is None:
        raise RuntimeError(_request_ctx_err_msg)
    return getattr(top, name)

也就是说 LocalProxy__local就是 partial(_lookup_req_object, "request")

那么 _get_current_object实际上就是执行 partial(_lookup_req_object, "request")()来获取栈顶对象

入栈

我们继续来看wsgi_app这个方法,我去掉了一些无关代码,那些部分会在其他博文中介绍

def wsgi_app(self, environ, start_response):
    # 调用了self.request_context这个方法
    # 此方法把environ封装成了RequestContext对象
    ctx = self.request_context(environ)      
def request_context(self, environ):
    # 注意request_context是Flask的类方法,那么self就是Flask的实例或者说对象
    # 也就是说下面的 RequestContext 的实例化需要Flask实例和environ参数
    return RequestContext(self, environ)

简单看一下RequestContext的定义,位置在源码的ctx.py

class RequestContext(object):
    def __init__(self, app, environ, request=None, session=None):
        self.app = app
        if request is None:
            request = app.request_class(environ)
        self.request = request

回到wsgi_app这个方法,我们看到接下来ctx.push这一句调用了RequestContextpush方法,这个方法就是把自身也就是 RequestContext实例压入到 LocalStack这个数据结构中

    def wsgi_app(self, environ, start_response):
        # ctx是 RequestContext 实例
        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:  # noqa: B001
                error = sys.exc_info()[1]
                raise
            return response(environ, start_response)
        finally:
            if self.should_ignore_error(error):
                error = None
            ctx.auto_pop(error)

push方法

    def push(self):
        top = _request_ctx_stack.top

_request_ctx_stack的定义在globals.py中,就是创建了一个空的本地栈

_request_ctx_stack = LocalStack()

它现在的状态是这样,栈是空的

{0:'stack':[]}

如果栈是空的话,我们把self也就是RequestContext压入栈(最后一句),注意pushRequestContext的方法,所以self就是RequestContext的实例

    def push(self):
        top = _request_ctx_stack.top
        
        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)


        # 压入栈
        _request_ctx_stack.push(self)

我们还注意到这里还有一个_app_ctx_stack,这也是LocalStack,位置在globals.py,他现在也是空的

_app_ctx_stack = LocalStack()

只不过这个栈里面存储的是应用上下文,类似下面的字典

{0:'stack':[AppContext]}

然后我们也执行了app_ctx.push()方法,也就是把应用上下文压入栈

到这里我们就知道了,执行RequestContext对象的push方法会把RequestContext的实例压入_request_ctx_stack中,还会把AppContext的实例压入_app_ctx_stack

为什么要用LocalProxy

我们常常需要在一个视图函数中获取客户端请求中的参数,例如urlremote_address

我们当然可以每次手动获取_request_ctx_stack栈顶的RequestContext对象,然后调用RequestContextrequest属性,但每次操作栈结构还是有点繁琐,像下面这样

from flask import Flask, request, Request
from flask.globals import _request_ctx_stack

flask_app = Flask(__name__)


@flask_app.route('/')
def hello_world():
    req: Request = _request_ctx_stack.top.request
    remote_addr=req.remote_addr
    return "{}".format(remote_addr)

flask的做法是使用LocalProxy

from flask import Flask, request

我们执行request.remote_addr就相当于执行 _request_ctx_stack.top.request.remote_addr

那为什么用LocalProxy而不是直接request=_request_ctx_stack.top.request呢?

原因是这样写,项目run的时候,这句 request=_request_ctx_stack.top.request 就已经执行了(因为被引用了),但是项目启动的时候_request_ctx_stack.top还是None,因为还没有请求进来,push方法还没执行。这就导致了request固定成了None,这显然不行

LocalProxy 重写了__getattr__方法,让每次执行 request.remote_addr会先去 LocalStack中拿到 RequestContext,然后执行 RequestContext.request.remote_addr,获取其他属性也是一样

也就是说,代理模式延迟了 被代理对象的获取,代理对象Localproxy创建的时候不会获取,获取被代理对象属性的时候才会获取被代理对象

总结

到这里,我们就可以看出flask中的请求上下文是如何存储和获取的

请求上下文存储在LocalStack结构中,Local是为了线程安全,LocalStack是为了多请求上下文的场景

而获取是通过LocalProxy,使用代理模式是为了动态地获取请求上下文,在访问request属性的时候,才会从栈中获取真实的请求上下文,然后代理 属性的获取

flask中还有应用上下文,current_app,我们有时候会通过current_app.config来获取配置信息,原理和request类似,只不过代理的是AppContext对象

说了这么多,wsgi_app的第一步,也就是请求的第一步,就是先把请求上下文和应用上下文入栈

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容