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

2,491 阅读8分钟

前言

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

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

回顾Flask上下文

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

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

为何需要werkzeug库的Local类?

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

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时,用户可以单独构建新的上下文,将一个上下文以请求上下文和应用上下文的形式分开,可以让用户单独创建其中一种上下文,这很方便用户在不同的情景使用不同的上下文。

为什么要使用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 源码解析:上下文