Flask源码剖析(四):Flask的上下文机制(下)

前言

本文紧接着「Flask源码剖析(三):Flask的上下文机制(上)」,讨论以下问题。前端

  • 1.Python中有thread.local了,werkzeug为何还要本身弄一个Local类来存储数据?
  • 2.为何不构建一个上下文而是要将其分为请求上下文(request context)和应用上下文(application context)?
  • 3.为何不直接使用Local?而要经过LocalStack类将其封装成栈的操做?
  • 4.为何不直接使用LocalStack?而要经过LocalProxy类来代理操做?

回顾Flask上下文

在上一篇文章中,详细讨论了Flask上下文机制,这里简单回顾一下。python

所谓Flask上下文,其实就是基于list实现的栈,这个list存放在Local类实例化的对象中,Local类利用线程id做为字典的key,线程具体的值做为字典的values来实现线程安全,使用的过程就是出栈入栈的过程,此外,在具体使用时,会经过LocalProxy类将操做都代理给Local类对象。shell

为什么须要werkzeug库的Local类?

treading标准库中已经提供了local对象,该对象实现的效果与Local相似,以线程id为字典的key,将线程具体的值做为字典的values存储,简单使用以下。flask

In [1]: import threading

In [2]: local = threading.local()

In [3]: local.name = '二两'

In [4]: local.name
Out[4]: '二两'
复制代码

那为什么werkzeug库要本身再实现一个功能相似的Local类呢?后端

主要缘由是为了兼容协程,当用户经过greenlet库来构建协程时,由于多个协程能够在同一个线程中,threading.local没法处理这种状况,而Local能够经过getcurrent()方法来获取协程的惟一标识。浏览器

# werkzeug/local.py

# 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
复制代码

为何要将上下文分为多个?

回顾一下问题。安全

为何不构建一个上下文而是要将其分为请求上下文(request context)和应用上下文(application context)?服务器

为了「灵活度」。数据结构

虽然在实际的Web项目中,每一个请求只会对应一个请求上下文和应用上下文,但在debug或使用flask shell时,用户能够单独构建新的上下文,将一个上下文以请求上下文和应用上下文的形式分开,可让用户单首创建其中一种上下文,这很方便用户在不一样的情景使用不一样的上下文。app

为何要使用LocalStack?

回顾一下问题。

为何不直接使用Local?而要经过LocalStack类将其封装成栈的操做?

在StackoverFlow上能够搜到相应的答案。总结而言,经过LocalStack实现栈结构而不直接使用Local的目的是为了在多应用情景下让一个请求能够很简单的知道当前上下文是哪一个。

要理解这个回答,先要回顾一下Flask多应用开发的内容并将其与上下文的概念结合在一块儿理解。

Flask多应用开发的简单例子以下。

from werkzeug.wsgi import DispatcherMiddleware
from werkzeug.serving import run_simple
from flask import Flask

frontend = Flask('frontend')
backend = Flask('backend')

@frontend.route('/home')
def home():
    return 'frontend home'

@backend.route('/home')
def home():
    return 'backend home'

"""默认使用frontend,访问 127.0.0.1:5000/home 返回 frontend url以forntend开头时,使用frontend, 访问 127.0.0.1:5000/frontend/home 返回 frontend url以backend开头时,使用backend 访问 127.0.0.1:5000/backend/home 返回 backend"""
app = DispatcherMiddleware(frontend, {
    '/frontend':     frontend
    '/backend':     backend
})

if __name__ == '__main__':
    run_simple('127.0.0.1', 5000, app)
复制代码

利用werkzeug的DispatcherMiddleware,让一个Python解释器能够同时运行多个独立的应用实例,其效果虽然跟使用蓝图时的效果相似,但要注意,此时是多个独立的Flask应用,具体而言,每一个独立的Flask应用都建立了本身的上下文。

每一个独立的Flask应用都是一个合法的WSGI应用,利用DispatcherMiddleware,经过调度中间件的逻辑将多个Flask应用组合成一个大应用。

简单理解Flask多应用后,回顾一下Flask上下文的做用。好比,要得到当前请求的path属性,能够经过以下方式。

from flask import request

print(request.path)
复制代码

Flask在多应用的状况下,依旧能够经过request.path得到当前应用的信息,实现这个效果的前提就是,Flask知道当前请求对应的上下文。

栈结构很好的实现了这个前提,每一个请求,其相关的上下文就在栈顶,直接将栈顶上下文出栈就能够得到当前请求对应上下文中的信息了。

有点抽象?以上面的Flask多应用的代码举个具体的例子。

在上面Flask多应用的代码中,构建了frontend应用与backend应用,两个应用相互独立,分别负责前端逻辑与后端逻辑,经过DispatcherMiddleware将其整合在一块儿,这种状况下,_app_ctx_stack栈中就会有两个应用上下文。

访问127.0.0.1:5000/backend/home时,backend应用上下文入栈,成为栈顶。想要获取当前请求中的信息时,直接出栈就能够得到与当前请求对应的上下文信息。

须要注意,请求上下文、应用上下文是具体的对象,而_request_ctx_stack(请求上下文栈)与_app_ctx_stack(应用上下文栈)是数据结构,再次看一下LocalStack类关于建立栈的代码。

# werkzeug/local.py

class LocalStack(object):
    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()
复制代码

能够发现,所谓栈就是一个list,结合Local类的代码,上下文堆栈其结构大体为{thread.get_ident(): []},每一个线程都有独立的一个栈。

此外,Flask基于栈结构能够很容易实现内部重定向。

  • 外部重定向:用户经过浏览器请求URL-1后,服务器返回302重定向请求,让其请求URL-2,用户的浏览器会发起新的请求,请求新的URL-2,得到相应的数据。
  • 内部重定向:用户经过浏览器请求URL-1后,服务器内部之间将ULR-2对应的信息直接返回给用户。

Flask在内部经过屡次入栈出栈的操做能够很方便的实现内部重定向。

为何要使用LocalProxy?

回顾一下问题。

为何不直接使用LocalStack?而要经过LocalProxy类来代理操做?

这是由于Flask的上下文中保存的数据都是存放在栈里而且会动态变化的,经过LocalProxy能够动态的访问相应的对象,从而避免形成数据访问异常。

怎么理解?看一个简单的例子,首先,直接操做LocalStack,代码以下。

from werkzeug.local import LocalStack

l_stack = LocalStack()
l_stack.push({'name': 'ayuliao'})
l_stack.push({'name': 'twotwo'})

def get_name():
    return l_stack.pop()

name = get_name()

print(f"name is {name['name']}")
print(f"name is {name['name']}")
复制代码

运行上述代码,输出的结果以下。

name is twotwo
name is twotwo
复制代码

能够发现,结果相同。

利用LocalProxy代理操做,代码以下。

from werkzeug.local import LocalStack, LocalProxy

l_stack = LocalStack()
l_stack.push({'name': 'ayuliao'})
l_stack.push({'name': 'twotwo'})

def get_name():
    return l_stack.pop()

# 代理操做get_name
name2 = LocalProxy(get_name)
print(f"name is {name2['name']}")
print(f"name is {name2['name']}")
复制代码

运行上述代码,输出的结果以下。

name is twotwo
name is ayuliao
复制代码

经过LocalProxy代理操做后,结果不一样。

经过LocalProxy代理操做后,每一次获取值的操做其实都会调用__getitem__,该方法是个匿名函数,x就是LocalProxy实例自己,这里即为name2,而i则为查询的属性,这里即为name。

class LocalProxy(object):    
    # ... 省略部分代码
    __getitem__ = lambda x, i: x._get_current_object()[i]
复制代码

结合__init___get_current_object()方法来看。

class LocalProxy(object): 
    def __init__(self, local, name=None):
        object.__setattr__(self, '_LocalProxy__local', local)
        object.__setattr__(self, '__name__', name)
        if callable(local) and not hasattr(local, '__release_local__'):
            object.__setattr__(self, '__wrapped__', local)

    def _get_current_object(self):
        if not hasattr(self.__local, '__release_local__'):
            return self.__local() # 再次执行get_name
        try:
            return getattr(self.__local, self.__name__)
        except AttributeError:
            raise RuntimeError('no object bound to %s' % self.__name__
复制代码

__init__方法中,将get_name赋值给了_LocalProxy__local,由于get_name不存在__release_local__属性,此时使用_get_current_object()方法,至关于再次执行ge_name(),出栈后得到新的值。

经过上面的分析,明白了经过LocalProxy代理后,调用两次name['name']获取的值不一样的缘由。

那为何要这样作?看到Flask中globals.py的部分代码。

# flask/globals.py

current_app = LocalProxy(_find_app)
复制代码

当前应用current_app是经过LocalProxy(_find_app)得到的,即每次调用current_app()会执行出栈操做,得到与当前请求相对应的上下文信息。

若是current_app = _find_app(),此时current_app就不会再变化了,在多应用多请求的状况下是不合理的,会抛出相应的异常。

总结

最后,以简单的话来总结一下上面的讨论。

问:Python中有thread.local了,werkzeug为何还要本身弄一个Local类来存储数据?

答:werkzeug的Local类支持协程。

问:为何不构建一个上下文而是要将其分为请求上下文(request context)和应用上下文(application context)?

答:为了「灵活度」。

问:为何不直接使用Local?而要经过LocalStack类将其封装成栈的操做?

答:为了在多应用情景下让一个请求能够很简单的知道当前上下文是哪一个。此外栈的形式易于Flask内部重定向等操做的实现。

问:为何不直接使用LocalStack?而要经过LocalProxy类来代理操做?

答:由于Flask的上下文中保存的数据都是存放在栈里而且会动态变化的,经过LocalProxy能够动态的访问相应的对象。

结尾

Flask上下文的内容就介绍完了,其实主要的逻辑在Werkzeug上,讨论了Local、LocalStack、LocalProxy,后面将继续剖析Flask源码,但愿喜欢。

若是文章对你有启发、有帮助,点击「在看」支持一下二两,让我有分享的动力。

参考

What is the purpose of Flask's context stacks?

Flask上下文相关文档

flask 源码解析:上下文

相关文章
相关标签/搜索