Flask的上下文堆栈的目的是什么?

时间:2013-11-17 21:39:15

标签: python flask

我一直在使用请求/应用程序上下文一段时间没有完全理解它是如何工作的,或者为什么它的设计方式如此。当涉及请求或应用程序上下文时,“堆栈”的目的是什么?这两个独立的堆栈,还是它们都是一个堆栈的一部分?请求上下文是否被压入堆栈,还是堆栈本身?我可以在彼此之上推送/弹出多个上下文吗?如果是这样,我为什么要这样做?

对于所有问题感到抱歉,但在阅读了请求上下文和应用程序上下文的文档后,我仍然感到困惑。

4 个答案:

答案 0 :(得分:202)

多个应用

应用程序上下文(及其目的)确实令人困惑,直到您意识到Flask可以拥有多个应用程序。想象一下,您希望单个WSGI Python解释器运行多个Flask应用程序的情况。我们不在这里谈论蓝图,我们正在谈论完全不同的Flask应用程序。

您可以将此设置类似于Flask documentation section on "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"和"后端"。换句话说,Flask(...)应用程序构造函数已被调用两次,创建了两个Flask应用程序实例。

上下文

当您使用Flask时,通常最终会使用全局变量来访问各种功能。例如,您可能有代码读取...

from flask import request

然后,在视图中,您可以使用request来访问当前请求的信息。显然,request不是正常的全局变量;实际上,它是context local值。换句话说,幕后有一些魔术说#34;当我调用request.path时,从CURRENT请求的path对象中获取request属性。&#34 ;对于request.path,两个不同的请求会产生不同的结果。

事实上,即使您使用多个线程运行Flask,Flask也足够聪明,可以隔离请求对象。这样做,每个处理不同请求的两个线程可以同时调用request.path并获取各自请求的正确信息。

将它放在一起

因此我们已经看到Flask可以在同一个解释器中处理多个应用程序,而且由于Flask允许您使用" context local"全局必须有一些机制来确定"当前" request 是(为了执行request.path)。

将这些想法放在一起,也应该让Flask必须有某种方法来确定"当前"申请是!

您可能还有类似以下代码:

from flask import url_for

与我们的request示例一样,url_for函数具有依赖于当前环境的逻辑。然而,在这种情况下,很明显逻辑很大程度上取决于哪个应用程序被认为是当前的"应用程序。在上面显示的前端/后端示例中,"前端"和"后端"应用程序可以有一个" / login"路由,因此url_for('/login')应返回不同的内容,具体取决于视图是否正在处理前端或后端应用程序的请求。

回答你的问题......

  

"堆栈"的目的是什么?当涉及到请求或   应用程序上下文?

来自请求上下文文档:

  

因为请求上下文在内部维护为堆栈   可以多次推送和弹出。这非常方便实施   内部重定向等内容。

换句话说,即使你通常会在这些"当前"的堆叠上有0或1个项目。请求或"当前"应用程序,你可能有更多。

给出的示例是您的请求将返回"内部重定向"的结果。假设用户请求A,但您想要返回给用户B.在大多数情况下,您向用户发出重定向,并将用户指向资源B,这意味着用户将运行第二个请求获取B.稍微不同的处理方法是进行内部重定向,这意味着在处理A时,Flask将为资源B向自己发出新请求,并使用第二个请求的结果作为结果。用户的原始请求。

  

这两个独立的堆栈,还是它们都是一个堆栈的一部分?

他们是two separate stacks。但是,这是一个实现细节。更重要的不是有一个堆栈,而是事实上你可以随时获得当前的"应用或请求(堆栈顶部)。

  

请求上下文是否被压入堆栈,还是堆栈本身?

A"请求上下文"是"请求上下文堆栈的一项"。与" app context"类似。和" app context stack"。

  

我可以在彼此之上推送/弹出多个上下文吗?如果是这样,   为什么我要这样做?

在Flask应用程序中,您通常不会这样做。您可能想要的一个示例是内部重定向(如上所述)。然而,即使在这种情况下,你可能最终会让Flask处理一个新的请求,因此Flask会为你做所有的推/弹。

但是,在某些情况下,您希望自己操纵堆栈。

在请求之外运行代码

人们遇到的一个典型问题是他们使用Flask-SQLAlchemy扩展来使用如下所示的代码来设置SQL数据库和模型定义......

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

然后他们在应该从shell运行的脚本中使用appdb值。例如," setup_tables.py"脚本...

from myapp import app, db

# Set up models
db.create_all()

在这种情况下,Flask-SQLAlchemy扩展知道app应用程序,但在create_all()期间,它会抛出一个错误,抱怨没有应用程序上下文。这个错误是合理的;你从来没有告诉Flask在运行create_all方法时它应该处理什么应用程序。

您可能想知道为什么在视图中运行类似的功能时,您最终不需要进行此with app.app_context()调用。原因是Flask在处理实际的Web请求时已经为您处理了应用程序上下文的管理。问题实际上只出现在这些视图函数(或其他此类回调)之外,例如在一次性脚本中使用模型时。

解决方案是自己推送应用程序上下文,这可以通过执行...

来完成
from myapp import app, db

# Set up models
with app.app_context():
    db.create_all()

这会推送一个新的应用程序上下文(使用app的应用程序,记住可能有多个应用程序)。

测试

您希望操作堆栈的另一种情况是进行测试。您可以创建一个处理请求的单元测试,并检查结果:

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:
            # You can now view attributes on request context stack by using `request`.

        # Now the request context stack is empty

答案 1 :(得分:33)

以前的答案已经很好地概述了在请求期间Flask背景中发生的事情。如果你还没读过,我会在阅读之前推荐@MarkHildreth的答案。简而言之,为每个http请求创建一个新的上下文(线程),这就是为什么必须有一个允许Local和{{1}等对象的线程request工具的原因。可以跨线程全局访问,同时保持其请求特定的上下文。此外,在处理http请求时,Flask可以模拟来自内部的其他请求,因此需要将它们各自的上下文存储在堆栈上。此外,Flask允许多个wsgi应用程序在单个进程中相互运行,并且在请求期间可以调用多个操作(每个请求创建一个新的应用程序上下文),因此需要为应用程序提供上下文堆栈。这是以前答案中所涵盖内容的摘要。

我现在的目标是通过解释如何 Flask和Werkzeug如何处理这些上下文本地化来补充我们当前的理解。我简化了代码以增强对其逻辑的理解,但是如果你得到这个,你应该能够轻松掌握实际来源(gwerkzeug.local)中的大部分内容。

让我们首先了解Werkzeug如何实现线程本地。

本地

当http请求进入时,它将在单个线程的上下文中处理。作为在http请求期间生成新上下文的另一种方法,Werkzeug还允许使用greenlet(一种更轻的"微线程")而不是普通线程。如果您没有安装greenlet,它将恢复使用线程。这些线程(或greenlet)中的每一个都可以通过唯一ID进行标识,您可以使用模块的flask.globals函数检索该ID。该函数是获得get_ident()requestcurrent_appurl_for以及其他此类上下文绑定全局对象背后的魔力的起点。

g

现在我们拥有了我们的身份功能,我们可以知道在任何给定时间我们处于哪个线程,我们可以创建称为线程try: from greenlet import get_ident except ImportError: from thread import get_ident 的线程,这是一个可以被访问的上下文对象全局,但是当您访问其属性时,它们会解析为该特定线程的值。 e.g。

Local

这两个值同时存在于全局可访问的# globally local = Local() # ... # on thread 1 local.first_name = 'John' # ... # on thread 2 local.first_name = 'Debbie' 对象中,但在线程1的上下文中访问Local将为您提供local.first_name,而它将返回{{ 1}}在线程2上。

怎么可能?让我们看一些(简化的)代码:

'John'

从上面的代码我们可以看到神奇归结为'Debbie',它标识了当前的greenlet或线程。然后,class Local(object) def __init__(self): self.storage = {} def __getattr__(self, name): context_id = get_ident() # we get the current thread's or greenlet's id contextual_storage = self.storage.setdefault(context_id, {}) try: return contextual_storage[name] except KeyError: raise AttributeError(name) def __setattr__(self, name, value): context_id = get_ident() contextual_storage = self.storage.setdefault(context_id, {}) contextual_storage[name] = value def __release_local__(self): context_id = get_ident() self.storage.pop(context_id, None) local = Local() 存储将其用作将任何数据上下文存储到当前线程的密钥。

每个流程可以包含多个get_ident()个对象,LocalLocalrequest可以简单地创建其他对象。但这并不是它在Flask中完成的方式,它们不是技术上 g个对象,而是更准确的current_app个对象。什么是Local

LocalProxy

LocalProxy是一个查询LocalProxy以查找另一个感兴趣对象(即它代理的对象)的对象。我们来看看:

LocalProxy

现在要创建全局可访问的代理

Local

现在在请求过程的早些时候,您将在本地内部存储一些以前创建的代理可以访问的对象,无论我们在哪个线程

class LocalProxy(object):
    def __init__(self, local, name):
        # `local` here is either an actual `Local` object, that can be used
        # to find the object of interest, here identified by `name`, or it's
        # a callable that can resolve to that proxied object
        self.local = local
        # `name` is an identifier that will be passed to the local to find the
        # object of interest.
        self.name = name

    def _get_current_object(self):
        # if `self.local` is truly a `Local` it means that it implements
        # the `__release_local__()` method which, as its name implies, is
        # normally used to release the local. We simply look for it here
        # to identify which is actually a Local and which is rather just
        # a callable:
        if hasattr(self.local, '__release_local__'):
            try:
                return getattr(self.local, self.name)
            except AttributeError:
                raise RuntimeError('no object bound to %s' % self.name)

        # if self.local is not actually a Local it must be a callable that 
        # would resolve to the object of interest.
        return self.local(self.name)

    # Now for the LocalProxy to perform its intended duties i.e. proxying 
    # to an underlying object located somewhere in a Local, we turn all magic
    # methods into proxies for the same methods in the object of interest.
    @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

    # ... etc etc ... 

    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]

    # ... and so on ...

    __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

    # ... and so forth ...

使用# this would happen some time near application start-up local = Local() request = LocalProxy(local, 'request') g = LocalProxy(local, 'g') 作为全局可访问对象而不是自己使用# this would happen early during processing of an http request local.request = RequestContext(http_environment) local.g = SomeGeneralPurposeContainer() 的优点是它简化了管理。您只需要一个LocalProxy对象来创建许多全局可访问的代理。在请求结束时,在清理期间,您只需释放一个Locals(即您从其存储中弹出context_id)并且不必担心代理,它们仍然可以全局访问,仍然推迟Local为后续的http请求找到感兴趣的对象。

Local

为了在我们已经拥有Local时简化# this would happen some time near the end of request processing release(local) # aka local.__release_local__() 的创建,Werkzeug实现了LocalProxy魔术方法,如下所示:

Local

但是,如果您查看Flask源代码(flask.globals),那仍然不是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') requestg的创建方式。正如我们已经建立的那样,Flask可以产生多个&#34;假的&#34;请求(来自单个真实的http请求)并且在该过程中还推送多个应用程序上下文。这不是一个常见的用例,但它是框架的一种功能。由于这些&#34;并发&#34;请求和应用程序仍然限制为只有一个具有&#34;焦点&#34;在任何时候,将堆栈用于各自的上下文是有意义的。每当生成新请求或调用其中一个应用程序时,它们就会将其上下文推送到各自堆栈的顶部。 Flask为此目的使用current_app个对象。当他们结束他们的业务时,他们将上下文从堆栈中弹出。

LocalStack

这就是session的样子(同样简化了代码以便于理解其逻辑)。

LocalStack

从上面注意到LocalStack是存储在本地的堆栈,而不是存储在堆栈中的一堆本地存储。这意味着虽然堆栈是全局可访问的,但它在每个线程中都是不同的堆栈。

Flask没有将class LocalStack(object): def __init__(self): self.local = Local() 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) # this simply releases the 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 LocalStackrequestcurrent_app个对象直接解析为g,而是使用{ {1}}包装查找函数(而不是session对象)的对象,它将从LocalStack找到基础对象:

LocalProxy

所有这些都是在应用程序启动时声明的,但在将请求上下文或应用程序上下文推送到各自的堆栈之前,实际上不会解析任何内容。

如果你很想知道上下文是如何在堆栈中实际插入的(并随后弹出),请查看Local这是wsgi应用程序的入口点(即网络内容)服务器调用并在请求进入时将http环境传递给),并在LocalStack对象_request_ctx_stack = LocalStack() def _find_request(): top = _request_ctx_stack.top if top is None: raise RuntimeError('working outside of request context') return top.request request = LocalProxy(_find_request) def _find_session(): top = _request_ctx_stack.top if top is None: raise RuntimeError('working outside of request context') return top.session session = LocalProxy(_find_session) _app_ctx_stack = LocalStack() def _find_g(): top = _app_ctx_stack.top if top is None: raise RuntimeError('working outside of application context') return top.g g = LocalProxy(_find_g) def _find_app(): top = _app_ctx_stack.top if top is None: raise RuntimeError('working outside of application context') return top.app current_app = LocalProxy(_find_app) 之后创建flask.app.Flask.wsgi_app()对象。一旦被推到堆栈的顶部,就可以通过RequestContext访问它。这里有一些缩写代码来演示流程:

因此,您启动一​​个应用程序并将其提供给WSGI服务器......

push()

稍后会出现一个http请求,WSGI服务器会使用通常的参数调用该应用程序......

_request_ctx_stack

这大致是应用程序中发生的......

_request_ctx_stack.top

这与RequestContext ...

大致相同
app = Flask(*config, **kwconfig)

# ...

假设请求已完成初始化,那么从您的某个视图函数中查找app(environ, start_response) # aka app.__call__(environ, start_response) 将如下所示:

  • 从全球可访问的def 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() # ... 对象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.path(它注册为LocalProxy的函数)。
  • 该函数在request对象_find_request()中查询堆栈的顶层上下文。
  • 要查找顶部上下文,self.local对象首先查询其内部LocalStack属性(_request_ctx_stack)以查找先前存储在其中的LocalStack属性。
  • 来自Local它获得了顶级背景
  • self.local因此被解析为感兴趣的基础对象。
  • 从该对象中我们得到stack属性

所以我们已经看到了stacktop.requestpath的工作方式,现在想一想从Local中检索LocalProxy的含义和细微差别:

  • 一个LocalStack对象,它是一个简单的全局可访问对象。
  • 一个本地的path对象。
  • 存储为本地属性的request对象。
  • 一个request对象,它是存储在本地的对象的代理。
  • 存储在堆栈中的request对象,然后存储在本地。
  • 一个request对象,它是存储在本地的堆栈上的对象的代理。 &lt; - 这就是Flask所做的。

答案 2 :(得分:11)

很少添加 @Mark Hildreth 的回答。

上下文堆栈看起来像{thread.get_ident(): []},其中[]称为“堆栈”,因为仅使用appendpush),pop[-1]__getitem__(-1))运作。因此,上下文堆栈将保留线程或greenlet线程的实际数据。

current_appgrequestsession等是LocalProxy对象,它刚刚覆盖了特殊方法__getattr__,{{1} },__getitem____call__等,并通过参数名称(__eq__[-1])从上下文堆栈顶部(current_app)返回值。 request需要导入一次这个对象,他们不会错过现实。所以最好只导入LocalProxy,而不是代码,而是将请求参数发送到函数和方法。您可以轻松编写自己的扩展,但不要忘记,无聊的使用会使代码更难以理解。

花时间了解https://github.com/mitsuhiko/werkzeug/blob/master/werkzeug/local.py

那么两个堆栈的填充程度如何?根据要求request

  1. 按环境创建Flask(init request_context,匹配路径)
  2. 输入或推送此请求:
    1. 清除之前的map_adapter
    2. 创建request_context如果错过并推送到应用程序上下文堆栈
    3. 此请求被推送到请求上下文堆栈
    4. 如果错过了初始会话
  3. 派遣请求
  4. 清除请求并从堆栈中弹出

答案 3 :(得分:1)

让我们举一个例子,假设你想设置一个usercontext(使用Local和LocalProxy的flask结构)。

定义一个用户类:

class User(object):
    def __init__(self):
        self.userid = None

定义一个函数来检索当前线程或greenlet中的用户对象

def get_user(_local):
    try:
        # get user object in current thread or greenlet
        return _local.user
    except AttributeError:
        # if user object is not set in current thread ,set empty user object 
       _local.user = User()
    return _local.user

现在定义一个LocalProxy

usercontext = LocalProxy(partial(get_user, Local()))

现在在当前线程中获取用户的userid     usercontext.userid

解释:

1.Local有一个标识和objet的字典,标识是threadid或greenlet id,在这个例子中_local.user = User()与_local的相同.___ storage __ [当前线程的id] [“user”] = User( )

  1. LocalProxy 委托操作以包装Local对象,或者您可以提供返回目标对象的函数。在上面的示例中,get_user函数向LocalProxy提供当前用户对象,当您通过usercontext.userid请求当前用户的userid时,LocalProxy的__getattr__函数首先调用get_user来获取User对象(user),然后调用getattr(user,“userid”)。要在User上设置userid(在当前线程或greenlet中),您只需执行以下操作:usercontext.userid =“user_123”