专栏名称: Python开发者
人生苦短,我用 Python。伯乐在线旗下账号「Python开发者」分享 Python 相关的技术文章、工具资源、精选课程、热点资讯等。
目录
相关文章推荐
Python中文社区  ·  狂暴大牛市该买点啥?用量化选股抓住机会 ·  4 天前  
Python爱好者社区  ·  又一大企启动大规模自愿离职计划,一千多人报名 ... ·  1 周前  
Python爱好者社区  ·  1885页的Python完整版电子书 ·  1 周前  
Python爱好者社区  ·  yyds!Kaggle教程 ·  6 天前  
蚂蚁学Python  ·  有一个Python技术库,再怎么多学都不为过 ·  1 周前  
51好读  ›  专栏  ›  Python开发者

flask 源码解析:上下文

Python开发者  · 公众号  · Python  · 2017-03-13 20:28

正文

(点击上方蓝字,快速关注我们)


来源:cizixs

cizixs.com/2017/01/13/flask-insight-context

如有好文章投稿,请点击 → 这里了解详情


上下文(application context 和 request context)


上下文一直是计算机中难理解的概念,在知乎的一个问题下面有个很通俗易懂的回答:


每一段程序都有很多外部变量。只有像Add这种简单的函数才是没有外部变量的。一旦你的一段程序有了外部变量,这段程序就不完整,不能独立运行。你为了使他们运行,就要给所有的外部变量一个一个写一些值进去。这些值的集合就叫上下文。

— vzch


比如,在 flask 中,视图函数需要知道它执行情况的请求信息(请求的 url,参数,方法等)以及应用信息(应用中初始化的数据库等),才能够正确运行。


最直观地做法是把这些信息封装成一个对象,作为参数传递给视图函数。但是这样的话,所有的视图函数都需要添加对应的参数,即使该函数内部并没有使用到它。


flask 的做法是把这些信息作为类似全局变量的东西,视图函数需要的时候,可以使用 from flask import request 获取。但是这些对象和全局变量不同的是——它们必须是动态的,因为在多线程或者多协程的情况下,每个线程或者协程获取的都是自己独特的对象,不会互相干扰。


那么如何实现这种效果呢?如果对 python 多线程比较熟悉的话,应该知道多线程中有个非常类似的概念 threading.local,可以实现多线程访问某个变量的时候只看到自己的数据。内部的原理说起来也很简单,这个对象有一个字典,保存了线程 id 对应的数据,读取该对象的时候,它动态地查询当前线程 id 对应的数据。flaskpython 上下文的实现也类似,后面会详细解释。


flask 中有两种上下文:application context 和 request context。上下文有关的内容定义在 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)

 

 

def _lookup_app_object(name):

    top = _app_ctx_stack.top

    if top is None:

        raise RuntimeError(_app_ctx_err_msg)

    return getattr(top, name)

 

 

def _find_app():

    top = _app_ctx_stack.top

    if top is None:

        raise RuntimeError(_app_ctx_err_msg)

    return top.app

 

 

# context locals

_request_ctx_stack = LocalStack()

_app_ctx_stack = LocalStack()

current_app = LocalProxy(_find_app)

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

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

g = LocalProxy(partial(_lookup_app_object, 'g'))


flask 提供两种上下文:application context 和 request context 。app lication context 又演化出来两个变量 current_app 和 g,而 request context 则演化出来 request 和 session。


这里的实现用到了两个东西:LocalStack 和 LocalProxy。它们两个的结果就是我们可以动态地获取两个上下文的内容,在并发程序中每个视图函数都会看到属于自己的上下文,而不会出现混乱。


LocalStack 和 LocalProxy 都是 werkzeug 提供的,定义在 local.py 文件中。在分析这两个类之前,我们先介绍这个文件另外一个基础的类 Local。Local 就是实现了类似 threading.local 的效果——多线程或者多协程情况下全局变量的隔离效果。下面是它的代码:


# since each thread has its own greenlet we can just use those as identifiers

# for the context.  If greenlets are not available we fall back to the

# current thread ident depending on where it is.

try:

    from greenlet import getcurrent as get_ident

except ImportError:

    try:

        from thread import get_ident

    except ImportError:

        from _thread import get_ident

 

class Local(object):

    __slots__ = ('__storage__', '__ident_func__')

 

    def __init__(self):

        # 数据保存在 __storage__ 中,后续访问都是对该属性的操作

        object.__setattr__(self, '__storage__', {})

        object.__setattr__(self, '__ident_func__', get_ident)

 

    def __call__(self, proxy):

        """Create a proxy for a name."""

        return LocalProxy(self, proxy)

 

    # 清空当前线程/协程保存的所有数据

    def __release_local__(self):

        self.__storage__.pop(self.__ident_func__(), None)

 

    # 下面三个方法实现了属性的访问、设置和删除。

    # 注意到,内部都调用 `self.__ident_func__` 获取当前线程或者协程的 id,然后再访问对应的内部字典。

    # 如果访问或者删除的属性不存在,会抛出 AttributeError。

    # 这样,外部用户看到的就是它在访问实例的属性,完全不知道字典或者多线程/协程切换的实现

    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}

 

    def __delattr__(self, name):

        try:

            del self.__storage__[self.__ident_func__()][name]

        except KeyError:

            raise AttributeError(name)


可以看到,Local 对象内部的数据都是保存在 __storage__ 属性的,这个属性变量是个嵌套的字典:map[ident]map[key]value。最外面字典 key 是线程或者协程的 identity,value 是另外一个字典,这个内部字典就是用户自定义的 key-value 键值对。用户访问实例的属性,就变成了访问内部的字典,外面字典的 key 是自动关联的。__ident_func 是 协程的 get_current 或者线程的 get_ident,从而获取当前代码所在线程或者协程的 id。


除了这些基本操作之外,Local 还实现了 __release_local__ ,用来清空(析构)当前线程或者协程的数据(状态)。__call__ 操作来创建一个 LocalProxy 对象,LocalProxy 会在下面讲到。


理解了 Local,我们继续回来看另外两个类。


LocalStack 是基于 Local 实现的栈结构。如果说 Local 提供了多线程或者多协程隔离的属性访问,那么 LocalStack 就提供了隔离的栈访问。下面是它的实现代码,可以看到它提供了 push、pop 和 top 方法。


__release_local__ 可以用来清空当前线程或者协程的栈数据,__call__ 方法返回当前线程或者协程栈顶元素的代理对象。


class LocalStack(object):

    """This class works similar to a :class:`Local` but keeps a stack

    of objects instead. """

 

    def __init__(self):

        self._local = Local()

 

    def __release_local__(self):

        self._local.__release_local__()

 

    def __call__(self):

        def _lookup():

            rv = self.top

            if rv is None:

                raise RuntimeError('object unbound')

            return rv

        return LocalProxy(_lookup)

 

    # push、pop 和 top 三个方法实现了栈的操作,

    # 可以看到栈的数据是保存在 self._local.stack 属性中的

    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

 

    def pop(self):

        """Removes the topmost item from the stack, will return the

        old value or `None` if the stack was already empty.

        """

        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):

        """The topmost item on the stack.  If the stack is empty,

        `None` is returned.

        """

        try:

            return self._local.stack[-1]

        except (AttributeError, IndexError):

            return None


我们在之前看到了 request context 的定义,它就是一个 LocalStack 的实例:


_request_ctx_stack = LocalStack()


它会当前线程或者协程的请求都保存在栈里,等使用的时候再从里面读取。至于为什么要用到栈结构,而不是直接使用 Local,我们会在后面揭晓答案,你可以先思考一下。


LocalProxy 是一个 Local 对象的代理,负责把所有对自己的操作转发给内部的 Local 对象。LocalProxy 的构造函数介绍一个 callable 的参数,这个 callable 调用之后需要返回一个 Local 实例,后续所有的属性操作都会转发给 callable 返回的对象。


class LocalProxy(object):

    """Acts as a proxy for a werkzeug local.

    Forwards all operations to a proxied object. """

    __slots__ = ('__local', '__dict__', '__name__')

 

    def __init__(self, local, name=None):

        object.__setattr__(self, '_LocalProxy__local', local)

        object.__setattr__(self, '__name__', name)

 

    def _get_current_object(self):

        """Return the current object."""

        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__)

 

    @property

    def __dict__(self):

        try:

            return self._get_current_object().__dict__

        except RuntimeError:

            raise AttributeError('__dict__')

 

    def __getattr__(self, name):

        if name == '__members__':

            return dir(self._get_current_object())

        return getattr(self._get_current_object(), name)

 

    def __setitem__(self, key, value):

        self._get_current_object()[key] = value


这里实现的关键是把通过参数传递进来的 Local 实例保存在 __local 属性中,并定义了 _get_current_object() 方法获取当前线程或者协程对应的对象。


NOTE:前面双下划线的属性,会保存到 _ClassName__variable 中。所以这里通过 “_LocalProxy__local” 设置的值,后面可以通过 self.__local 来获取。关于这个知识点,可以查看 stackoverflow 的这个问题(http://stackoverflow.com/questions/1301346/the-meaning-of-a-single-and-a-double-underscore-before-an-object-name-in-python)。


然后 LocalProxy 重写了所有的魔术方法(名字前后有两个下划线的方法),具体操作都是转发给代理对象的。这里只给出了几个魔术方法,感兴趣的可以查看源码中所有的魔术方法。


继续回到 request context 的实现:


_request_ctx_stack = LocalStack()

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

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


再次看这段代码希望能看明白,_request_ctx_stack 是多线程或者协程隔离的栈结构,request 每次都会调用 _lookup_req_object 栈头部的数据来获取保存在里面的 requst context。


那么请求上下文信息是什么被放在 stack 中呢?还记得之前介绍的 wsgi_app() 方法有下面两行代码吗?


ctx = self.request_context(environ)

ctx.push()


每次在调用 app.__call__ 的时候,都会把对应的请求信息压栈,最后执行完请求的处理之后把它出栈。


我们来看看request_context, 这个 方法只有一行代码:


def request_context(self, environ):

    return RequestContext(self, environ)


它调用了 RequestContext,并把 self 和请求信息的字典 environ 当做参数传递进去。追踪到 RequestContext 定义的地方,它出现在 ctx.py 文件中,代码如下:



每个 request context 都保存了当前请求的信息,比如 request 对象和 app 对象。在初始化的最后,还调用了 match_request 实现了路由的匹配逻辑。


push 操作就是把该请求的 ApplicationContext(如果 _app_ctx_stack 栈顶不是当前请求所在 app ,需要创建新的 app context) 和 RequestContext 有关的信息保存到对应的栈上,压栈后还会保存 session 的信息; pop 则相反,把 request context 和 application context 出栈,做一些清理性的工作。


到这里,上下文的实现就比较清晰了:每次有请求过来的时候,flask 会先创建当前线程或者进程需要处理的两个重要上下文对象,把它们保存到隔离的栈里面,这样视图函数进行处理的时候就能直接从栈上获取这些信息。


NOTE:因为 app 实例只有一个,因此多个 request 共享了 application context。


到这里,关于 context 的实现和功能已经讲解得差不多了。还有两个疑惑没有解答。


  1. 为什么要把 request context 和 application context 分开?每个请求不是都同时拥有这两个上下文信息吗?

  2. 为什么 request context 和 application context 都有实现成栈的结构?每个请求难道会出现多个 request context 或者 application context 吗?


第一个答案是“灵活度”,第二个答案是“多 application”。虽然在实际运行中,每个请求对应一个 request context 和一个 application context,但是在测试或者 python shell 中运行的时候,用户可以单独创建 request context 或者 application context,这种灵活度方便用户的不同的使用场景;而且栈可以让 redirect 更容易实现,一个处理函数可以从栈中获取重定向路径的多个请求信息。application 设计成栈也是类似,测试的时候可以添加多个上下文,另外一个原因是 flask 可以多个 application 同时运行:


from werkzeug.wsgi import DispatcherMiddleware

from frontend_app import application as frontend

from backend_app import application as backend

 

application = DispatcherMiddleware(frontend, {

    '/backend':     backend

})


这个例子就是使用 werkzeug 的 DispatcherMiddleware 实现多个 app 的分发,这种情况下 _app_ctx_stack 栈里会出现两个 application context。


参考资料


  • advanced flask patterns by Armin Ronacher

  • Flask doc: The application context

  • Flask 的 Context 机制


看完本文有收获?请转发分享给更多人

关注「Python开发者」,提升Python技能