Flask上下文机制(Context)源码分析

Flask框架中有许多魔法将Web应用开发者与一些细节隔离开来,其中Context机制又是有别于其他框架的,这个机制让开发人员在处理web请求时可以非常简单的获取上下文。为了理解Context机制,下载了Flask的0.10版本的源码进行分析,虽然与最新的版本已经有了一点点区别,但是还是可以看出Flask作者最基本的设计思路。Context机制实现了应用上下文(App Context)、请求上下文(Request Context)、session、g这4个上下文。这4个上下文的作用以及使用方式就不介绍了,可以查看Flask官方文档,本文主要通过分析源码来介绍一些官方文档中没有说明的具体实现方式,以请求上下文为例,其他三个实现方式类似。

问题1:Request Context对象是如何保存的呢?

在写Flask视图代码时,我们会通过from flask import request 来引入Request Context,request 对象来自于flask/globals.py。可以看到:

from werkzeug.local import LocalStack, LocalProxy
request = LocalProxy(partial(_lookup_req_object, 'request'))

也就是说request是一个LocalProxy实例。LocalProxy对象很显然是一个代理类,那这个类代理的是什么呢?从from werkzeug.local import LocalProxy我们可以知道这个类来自于werkzeug,打开werkzeug.local可以看到:

class LocalProxy(object):
    def __init__(self, local, name=None):
        object.__setattr__(self, '_LocalProxy__local', local)
        object.__setattr__(self, '__name__', name)

    def _get_current_object(self):
        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__)

LocalProxy需要两个初始化属性('_LocalProxy__local' 以及 'name'),还定义了一个_get_current_object方法,这些就是这个类的关键了,'_LocalProxy__local' 是一个方法,通过调用这个方法可以获取真实的对象,而'name' 这个属性就是调用'_LocalProxy__local'所需的参数。_get_current_object负责真正去调用'_LocalProxy__local'方法(确切的说是可调用对象)并返回真实的对象。

回到request对象对照着来看一下,因为

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

所以request的真实对象是通过_lookup_req_object方法以及'request'参数来获取的。_lookup_req_object也定义在flask/globals.py,代码如下:

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

从上面的代码可以看出request对象实际上是_request_ctx_stack对象的top属性下的一个成员。那么_request_ctx_stack又是什么呢?同样在flask/globals.py,我们可以看到:

_request_ctx_stack = LocalStack()

也就是说_request_ctx_stack是LocalStack类的实例,那么LocalStack又是什么呢?这个得再回到werkzeug.local,我们可以看到LocalStack类,代码如下(为方便阅读,已经过删减):

class LocalStack(object):
    def __init__(self):    
        self._local = Local()

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

    @property
    def top(self):    
        try:        
             return self._local.stack[-1]    
        except (AttributeError, IndexError):        
             return None

可以看到_request_ctx_stack的top属性实际上是一个property装饰的方法,获取的是self._local.stack的最后一个元素,如果没有就返回None,而self._local则是Local的实例,到这里Request Context维持的关键实现要出现了,Local的代码如下(同样已经过删减):

class Local(object):
    def __init__(self):
        object.__setattr__(self, '__storage__', {})
        object.__setattr__(self, '__ident_func__', get_ident)

    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
        except KeyError:
            storage[ident] = {name: value}

可以看到Local的'__storage__'是一个dict,用key-value的形式来保存对象,key在__getattr__与__setattr__通过get_ident方法来获取(这是一个关键的方法,问题3中再讲),value同样也是一个dict,这个dict的key-value则是调用者(LocalStack)给的,接下来只要原路返回去看LocalStack的代码,可以发现LocalStack的push方法向Lock()的__storage__中的'stack'添加了一个obj对象,很显然obj就是真正的request对象了,我们在使用flask.request时实际上使用的就是这个对象(但我们还不知道这个对象是什么,问题2中会介绍)。
上面说的太复杂了,总结一下来说就是:request是通过LocalProxy代理的_request_ctx_stack对象(LocalStack实例)的_local属性(Local实例)中维持的一个字典内key为'stack'的值(一个数组)的栈顶的元素来保存。
再简单一点来说就是request放在一个栈里,我们通过获取栈顶元素来获取当前的request。那么request是什么时候添加到这个栈中的呢,为什么这个栈的栈顶一定是当前的request呢?请看问题2

问题2:Request Context的生命周期是怎么样的?

上面提到了LocalStack可以push Request Context到栈中, 那这个push方法很显然是在请求到flask主进程时执行的,所以先看下Flask类(在flask/app.py中),代码如下:

class Flask(_PackageBoundObject):
    request_class = Request
    ...
    def request_context(self, environ):
        return RequestContext(self, environ)

    def wsgi_app(self, environ, start_response):
        ctx = self.request_context(environ)
        ctx.push()
        error = None
        try:
            try:
                response = self.full_dispatch_request()
            except Exception as e:
                error = e
                response = self.make_response(self.handle_exception(e))
            return response(environ, start_response)
        finally:
            if self.should_ignore_error(error):
                error = None
            ctx.auto_pop(error)

可以看到Flask.request_class是Request类,这个类在flask/wrappers.py中实现,继承自werkzeug.wrappers.Request包含了请求的具体信息,这个就是request的真实对象了,但并不是Request Context,Request Context需要通过Flask.request_context方法获取,这个方法很简单返回了一个RequestContext实例,所以继续看RequestContext类的代码,如下(已经过删减):

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

   def push(self):
       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)

        if hasattr(sys, 'exc_clear'):
            sys.exc_clear()

        _request_ctx_stack.push(self)

   def pop(self):
      ....
   def auto_pop(self, exc):
      ....

可以看到RequestContext中的request是通过app.request_class(environ)返回的,app就是上面Flask类的实例,所以app.request_class就是Flask.request_class,到这里Request Context 就算生成了,而且从上面的代码中可以看到RequestContext中的push方法将Request Context push到了_request_ctx_stack中,这个push方法是在Flask.wsgi_app中调用的,所以问题到这里就清楚了。
总结一下Request Context的生命周期:Flask实例在获得外部请求时,调用实例app的wsgi_app方法生成RequestContext对象并push到_request_ctx_stack中,通过LocalStack来维持,请求处理过程中通过LocalProxy来获取,当请求处理完毕后调用RequestContext.auto_pop()删除Request Context。

问题3:Request Context如何做到thread/greenlet隔离?

这个问题的另一种说法是为什么通过flask.request获取始终是当前thread/greenlet
在问题1中提到了get_ident方法,这个方法是通过如下代码获取的:

try:
    from greenlet import getcurrent as get_ident
except ImportError:
    try:
        from thread import get_ident
    except ImportError:
        from _thread import get_ident

可以看到get_ident优先从greenlet.getcurrent获取,其次从thread获取。get_ident()其实返回了greenlet/thread的id,所以在_request_ctx_stack中的获取或者添加Request Context都是在这个id下面,所以就实现了隔离。
总结一下:通过LocalStack与Local的封装以及get_ident方法,_request_ctx_stack.top就始终指向当前thread/greenlet的Request Context。

其他3个Context:

上面分析了Request Context的源码,App Context的实现方式也是类似的,不同于Request Context,App Context维护在_app_ctx_stack中,但也是LocalStack的实例,代理实例则是current_app = LocalProxy(_find_app),_find_app与_lookup_req_object类似,具体代码如下:

def _find_app():
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return top.app

可以看出实现的方式是一样的。
g与session则是分别依附在App Context与Request Context中的,g维护在_app_ctx_stack里,代理实现如下:

def _lookup_app_object(name):
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return getattr(top, name)
g = LocalProxy(partial(_lookup_app_object, 'g'))

而session维护在_request_ctx_stack里,代理实现如下:

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

与Request Context的实现方式一致。
可以看出无论是App Context、Request Context、g还是session都是每个greenlet/thread一份,请求处理完后pop。所以App Context以及g是不会在请求间共享的,不要被他们的名字迷惑,g并非"global"。

最后一个问题:为什么要用栈来维护这些Context?

上面的分析看下来,应该还有一个疑问:所有的Context都是取相应栈的栈顶元素,既然只取一个元素为什么要先入栈再出栈这么麻烦,直接保存这个对象不就行了吗?
既然用了栈,说明这个栈里可能会有多个元素,举例来说:

  1. 对于_request_ctx_stack可能保存了多个请求的上下文对象,因为有时候我们需要用到"internal redirect",就是说A请求进来了,在视图函数里又发起了一个到B路由的请求,这样当前线程中的Request Context就要同时维护B请求与A请求,并且是先处理完B再处理完A。
  2. 同样的对于_app_ctx_stack,在有"internal redirect"时,App Context也需要是多份的。而栈这个数据结构的特点就是先进后出恰好符合了需求,所以要用栈。

最最后一个问题:为什么要有App Context?

Request Context以及session存在的目的是很显然的,因为每个请求需要不同的请求上下文与session,不然就乱套了,那为什么App Context也是每个请求一份呢?App Context里维护的是一个配置以及路由等信息,这些信息是不会随着请求变化的,难道App Context就是为了获取g这个对象?但是在Flask 0.10之前的版本里g其实是放在Request Context里的,所以也不是为g,而是有其它原因。
其实App Context存在的目的是为了实现多应用,参考Flask官网代码例子:

from werkzeug.wsgi import DispatcherMiddleware
from frontend_app import application as frontend
from backend_app import application as backend
application = DispatcherMiddleware(frontend, { '/backend': backend})

上面的代码就实现了在同一个WSGI服务中加载两个Flask实例,所以请求的App Context并不是一定的,需要给每个请求都带上一个App Context。
但是App Context与Request Context、sesson、g不一样的地方在于App Context里最终都是一个Flask实例,对于同一个Flask实例来说对象是不变的,多个请求发生时只是引用计数的改变,对象始终还是那一个,但Request Context、sesson、g则是每个请求开始处理之前新建对象,请求处理完了再由垃圾回收机制来回收,因为被pop了以后就引用计数就是0了,其实很好理解,尽管App Context生命周期与Request Context一样,但Flask实例与request对象的生命周期显然是不一样的。

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

推荐阅读更多精彩内容