Flask中上下文栈(context stacks)的目的?

[回答者Mark Hildreth]

Multiple Apps 多个应用

Flask可以有多个应用,如果没有了解到这一点,应用上下文的作用确实会令人迷惑。考虑一下这种场景:你想在一个WSGI python解释器运行多个Flask应用。这里我们讲的不是蓝本,而是完全不同的Flask应用。
一个应用分发(Application Dispatching)的例子

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

注意,这里有两个完全不同的Flask应用:frontend与backend(前端与后端)
换句话说,通过调用两次Flask(…)的构造方法来创建两个flask应用实例

Contexts 上下文

当你使用Flask时,需要经常使用全局变量访问不同的函数。例如,你可能会读到下边的代码:

from flask import request

在某个视图中,你可能会使用request对象访问当前请求的信息。显然,request不是全局变量。实际上,它是一个context local值。这里做了一个特殊处理,使得当我们请求request.path时能正确的从当前request的对象中获取path属性。

实际上,即使在Flask中运行多个线程,Flask也能够保持request对象的隔离性。在这种情况下,不同的线程可以处理各自的请求,并发的获取request.path信息,而且确保在他们各自的请求中能获取到正确的信息。

Putting it Together 综合考虑

我们已经看到Flask可以在同一个解释器中处理多个应用,这是由于Flask允许你使用”context local”全局变量来实现的。这其中肯定是有某种机制决定究竟哪个是当前的request对象。

综合以上情况,Flask一定有某种方法判断当前的应用是哪个。
你可能会碰到如下代码:

from flask import url_for

与request例子类似,url_for函数逻辑上是取决于当前环境。在这种情形下,可以十分肯定的说这种逻辑与Flask认定哪个app是当前的app有着紧密的关系。在上边的frontend/backend例子中,frontend与backend的app中可能都包含/login路由,url_for(‘/login’)会为不同app返回不同的值,这取决于视图正在处理的request(frontend或backend)

To answer your questions… 书归正传,回答问题

当我们谈到request或者应用上下文时,栈的目的是什么?

Request Context 文档:
由于在request上下文中维护者一个栈,因此你可以多次地出栈和入栈。这种方式十分有利于实现内部的重定向转发。
换句话说,你可以将多个内部的重定向请求放入“当前”requests或“当前”应用的栈中。

下边给出一个例子,你可以使request返回“内部重定向”的结果。例如一个用户对A发起请求,并将它返回的结果传给用户B。大多数情况下,你会给这个用户生成一个重定向请求,将这个用户重新定向到资源B,这意味着用户要运行第二个请求去获取B。与此对比。略微有些不同的处理方式是使用内部重定向,当处理A时,Flask会为资源B本身生成一个新的请求,将第二个请求的结果作为用户原始请求的结果。(笔者附注:不同体现在,第一种方法是程序员自己创建第二个request请求,而第二种是Flask框架自动创建这种request,并返回正确的结果给我们)

问题:这两个栈是分别独立的还是他们是一个栈的两部分?
他们是两个独立的栈。在任何时候你可以获得“当前”的应用或请求(位于栈的顶部)。源码:

flask.globals.py
……
#contextlocals
_request_ctx_stack=LocalStack()    #request 上下文栈
_app_ctx_stack=LocalStack()   # Flask应用上下文栈

问题:request上下文是被推进栈中,还是其本身就是一个栈?
一个请求上下文是request 上下文栈(_request_ctx_stack)的元素。“应用上下文”与”app context stack”之间的关系与之类似。

问题:可以在两个栈的顶部push/pop多个上下文吗?如果可以,何种情况下需要这么做?
在Flask应用中,你通常不需要这么做。举个例子,你可能会在处理内部重定向时想要这么做。但是,即使在这种情形下,你也应该利用Flask处理新请求,Flask会为你做好所有的push/pop操作。

有一些情形下你需要自己进行栈操作

Running code outside of a request 在请求之外运行代码

一个典型的例子是 ,当使用 Flask-SQLAlchemy扩展设置SQL数据库或者定义模型时。当使用类似下方的代码时,

app = Flask(__name__)
db = SQLAlchemy() # Initialize the Flask-SQLAlchemy extension object
db.init_app(app)

我们需要在shell中使用app和db的值时:

from myapp import app, db
# Set up models
db.create_all()

在这种情况下,Flask-SQLAlchemy扩展需要知道app应用的信息,但是当执行create_all()时,会抛出一个没有上下文的错误。

RuntimeError: application not registered on db instance and no application bound to current context

这个错误是正常的,这是由于在运行create_all()时,你没有告诉Flask这个应用需要处理的信息。
你可能疑惑,为何在视图中你运行相似的函数,没有使用with app.app_context()也没有出现错误呢。原因是,当你处理实际的web请求时,flask已经替你自动管理好应用的上下文。此类问题只会出现在:当有代码运行在视图函数(或类似的回调函数)以外的情形下。

解决方案是自己将应用上下文推入栈中,例如:

from myapp import app, db
# Set up models
with app.app_context():
    db.create_all()

测试

另一个需要手动操作栈的地方是测试。你可以创建一个单元测试用来处理请求和检查结果:

import unittest
from flask import request
class MyTest(unittest.TestCase):
    def test_thing(self):
        with app.test_request_context('/?next=http://example.com/') as ctx:
            # 你可以在这查看请求上下文栈的属性
#此处请求上下文栈会是空的

[回答者mike_e]

每个http请求都要创建一个上下文(线程),这是必须创建Local线程的原因。这样可以通过维护特定的request上下文,来保证request和g这样的对象可以被全局访问。此外,Flask在处理Http请求时,可以从内部模拟request,这就必须要求在一个栈中存储他们各自的上下文。Flask允许多个WSGI应用运行在单一进程中,并且在一个request请求中可能调用不止一个应用,因此必须为应用设计一个上下文栈。
我们首先来理解一下werkzeug如何实现Local线程。
Local
当发起一个 http请求时,某个线程的上下文会处理这个请求。也就是说,在http请求发起的同时会产生一个新的上下文。Werkzeug(__version__='0.12.2') 允许使用greenlets替代python的原生线程。如果没有安装greenlets会使用threads代替。每个线程都有一个唯一id作为标志符,get_ident()函数提供线程的检索功能。这个函数隐藏在request, current_app,url_for, g,等上下文绑定的全局对象中。

try:
    from greenlet import get_ident
except ImportError:
    from thread import get_ident

通过线程检索函数get_ident,我们可以很容易的知道当前的线程。我们可以创建一个叫做Local的线程,Local是一个上下文对象,可以被全局的访问。你可以访问特定线程的属性值。
例如:

# globally
local = Local()
# ...
# on thread 1
local.first_name = 'John'
# ...
# on thread 2
local.first_name = 'Debbie'

在同一时刻,可以通过访问Local来获取这两个线程的属性值。当查询local.first_name时,线程1的上下文会返回’John’,而线程2会返回’Debbie’

这是如何做到的呢?我们看一下Local的源码:

werkzeug.local.py
class Local(object):
    __slots__ = ('__storage__', '__ident_func__')

    def __init__(self):
        # 初始化线程字典__storage__,键:线程id,值:线程
        object.__setattr__(self, '__storage__', {})
        #get_ident 函数生成线程的Id
        object.__setattr__(self, '__ident_func__', get_ident)

     def __release_local__(self):
        self.__storage__.pop(self.__ident_func__(), None)

    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存储线程和相应的线程id.
键:线程id,值:线程。初始化时绑定字典和线程id生成函数。getattr是从字典中根据id取出线程。 setattr将特定的线程放入字典中(或者更新已有的值)。在Flask中并没有使用Local对象,使用的是LocalProxy 对象。

LocalProxy
class LocalProxy(object):
    def __init__(self, local, name):
       # local这里是一个实际的Local对象,可以用来查找特定的对象,其标识符是name.
       #local是可以调用的,可以确定代理对象
       self.local = local
       # 'name'作为标识符,传递给local来查找特定的对象
       self.name = name

    def _get_current_object(self):
        #如果self.local是一个Local对象,则其已经实现了__release_local__()方法,
        #正如其名字一样,通常用来释放Local对象
        #这里通过简单的查找来标记哪个是实际的Local对象,哪个是可调用的对象
        if hasattr(self.local, '__release_local__'):
        try:
            return getattr(self.local, self.name)
        except AttributeError:
            raise RuntimeError('no object bound to %s' % self.name)

        #如果self.local不是一个Local对象,则一定是可调用的对象
        #,这样可以决定用户感兴趣的对象
        return self.local(self.name)

    #现在LocalProxy 执行其特定的职责
    #比如,在Local中代理一个对象,我们将感兴趣的对象的魔幻方法
    #全部交给代理来处理
    @property
    def __dict__(self):
        try:
            return self._get_current_object().__dict__
        except RuntimeError:
            raise AttributeError('__dict__')
    def __repr__(self):
        try:
            return repr(self._get_current_object())
        except RuntimeError:
            return '<%s unbound>' % self.__class__.__name__
    def __bool__(self):
        try:
            return bool(self._get_current_object())
        except RuntimeError:
            return False
    # ...  等等 ... 
    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
    def __delitem__(self, key):
       del self._get_current_object()[key]
    # ... 等等 ...
    __setattr__ = lambda x, n, v: 
                  setattr(x._get_current_object(), n, v)
    __delattr__ = lambda x, n: 
                    delattr(x._get_current_object(), n)
    __str__ = lambda x: str(x._get_current_object())
    __lt__ = lambda x, o: x._get_current_object() < o
    __le__ = lambda x, o: x._get_current_object() <= o
    __eq__ = lambda x, o: x._get_current_object() == o
# ...  等等 …

现在你可以这样创建全局访问代理对象
# this would happen some time near application start-up
local = Local()
request = LocalProxy(local, 'request')
g = LocalProxy(local, 'g')

在request请求的前期,你可以在local(之前创建的代理)中存储一些对象,无论哪个线程都可以访问:

# this would happen early during processing of an http request
local.request = RequestContext(http_environment)
local.g = SomeGeneralPurposeContainer()

使用LocalProxy 为全局访问对象,相对直接使用Local对象而言,可以简化对象管理。你可以为一个单一的Local对象创建许多全局代理对象。在request请求的末期(清理阶段),你可以简单的释放一个Local(对其storage执行pop操作),并且这种操作不会影响代理,这些代理对象仍然可以全局的访问,并处理随后的http请求。

# this would happen some time near the end of request processing
release(local) # aka local.__release_local__()

当我们已经有一个Local对象时,为了简化创建代理LocalProxy ,Werkzeug 实现Local.call()方法的过程如下:

class Local(object):
    # ... 
    # ... all same stuff as before go here ...
    # ... 
    def __call__(self, name):
        return LocalProxy(self, name)
# now you can do
local = Local()
request = local('request')
g = local('g')

然而,如果查看Flask源码(flask.globals.py),你仍然无法知道request, g, current_app 和session等对象时如何创建。当我们创建应用时,Flask会同时创建多个”假的”request请求(从一个真正的http请求)并且在过程中push多个应用上下文。由于这些”并发”的请求和应用在任一时刻只有一个被处理。因此,有必要使用一个栈来储存他们各自的上下文。当一个新的请求产生或者一个应用被调用时,他们会将上下文push进各自的栈中。Flask使用LocalStack目的正是如此。当他们结束自己的业务时会从栈中弹出上下文。

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
    def pop(self):
        """移除栈顶的元素, 返回旧值或None.
            """
        stack = getattr(self.local, 'stack', None)
        if stack is None:
            return None
        elif len(stack) == 1:
            release_local(self.local) # 释放 local
            return stack[-1]
        else:
            return stack.pop()
    @property
    def top(self):
        """T获取栈顶的元素 """
        try:
            return self.local.stack[-1]
        except (AttributeError, IndexError):
            return None

注意,LocalStack是一个驻存在Local对象里的栈,并不是存储Local对象的栈。这意味着,尽管这个栈是全局可以访问的,但是其在彼此的线程中是不同的。
Flask并没有直接的从LocalStack中获取request, current_app, g, 和session等对象,而是包装了一层查找功能来寻找潜在的对象。

class LocalStack(object):
    …..
    """__version__='0.12.2'"""
    def __call__(self):
        def _lookup():
            rv = self.top
            if rv is None:
                raise RuntimeError('object unbound')
            return rv
        return LocalProxy(_lookup)
    …

#flask.globals.py   [__version__='0.12.2']

def _lookup_req_object(name):   #在request上下文栈中查找属性
    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'))

所有的上述对象,在应用建立的开始,栈中不会有任何的对象。除非你将一个request或者应用的上下文压入到他们各自的栈中。

如果你对上下文是如何插入栈中的细节感兴趣,可以参考源码flask.app.Flask.wsgi_app()。当登录到wsgi的应用时web服务器将http环境参数传递给request请求,随后创建RequestContext 对象。然后调用push()方法将上下文压入_request_ctx_stack栈中。一旦push到栈的顶部,就可以全局的访问_request_ctx_stack.top。这里列出上述流程的部分代码:
建立一个WSGI应用:

app = Flask(*config, **kwconfig)
# ...

随后http请求到达服务器,WSGI服务器调用app:

app(environ, start_response) # aka app.__call__(environ, start_response)
app中大致的处理过程如下:
class Flask(object):
# ...
def __call__(self, environ, start_response):
   return self.wsgi_app(environ, start_response)
def wsgi_app(self, environ, start_response):
   ctx = RequestContext(self, environ)
   ctx.push()
   try:
       # process the request here
       # raise error if any
       # return Response
   finally:
       ctx.pop()
# …

随后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.session = self.app.open_session(self.request)
        if self.session is None:
            self.session = self.app.make_null_session()
        self.flashes = None

    def push(self):
        _request_ctx_stack.push(self)

    def pop(self):
        _request_ctx_stack.pop()

当request 请求完成初始化后,接着在视图函数功能中查找request.path。随后的处理步骤如下:

• 流程的起点是全局可访问的LocalProxy 对象
• 在_lookup_req_object函数中查找特定的对象
• _lookup_req_object函数在_request_ctx_stack栈的顶部查找对象
• 为了查找顶部的上下文,LocalStack 对象首先检索内部的Local属性(self._local),并设置Localstack属性
• 从stack中获取顶top的上下文
• top.request可以决定客户端感兴趣的对象
• 从这个对象中获取path属性

我们已经知道Local, LocalProxy, 和 LocalStack的工作机制,现在总结一下:
request 对象是一个简单的全局可访问的对象。它是一个代理对象,储存在Local对象的属性stack中,即存储在stack的栈顶。


参考文献:

[1]https://stackoverflow.com/questions/20036520/what-is-the-purpose-of-flasks-context-stacks/20041823#20041823

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值