python 上下文管理器的魔法方法 flask 上下文_python 上下文管理器的魔法方法

前言

本文紧接着「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 将其整合在一起,这种情况下,appctx_stack 栈中就会有两个应用上下文。

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

需要注意,请求上下文、应用上下文是具体的对象,而 requestctxstack (请求上下文栈) 与 app ctxstack (应用上下文栈) 是数据结构,再次看一下 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__方法中,将 getname 赋值给了 _LocalProxy__local,因为 getname 不存在 __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)获得的,即每次调用 currentapp () 会执行出栈操作,获得与当前请求相对应的上下文信息。

如果 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 源码解析:上下文