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


157

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

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


5
kronosapiens.github.io/blog/2014/08/14/…IMO,这篇博客文章为我提供了关于烧瓶上下文的最容易理解的描述。
mission.liao

Answers:


242

多个应用

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

您可以按照“应用程序分派”示例中的Flask文档部分进行类似的设置:

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应用程序被创建为“前端”和“后端”。换句话说,Flask(...)应用程序构造函数被调用了两次,创建了Flask应用程序的两个实例。

语境

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

from flask import request

然后,在查看期间,您可能会request用来访问当前请求的信息。显然,request这不是正常的全局变量;实际上,它是上下文局部值。换句话说,幕后有一些魔术说“当我打电话时request.pathpathrequestCURRENT请求的对象中获取属性”。两个不同的请求将对产生不同的结果request.path

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

把它放在一起

因此,我们已经看到Flask可以在同一个解释器中处理多个应用程序,并且由于Flask允许您使用“上下文本地”全局变量的方式,因此必须有某种机制来确定“当前” 请求是什么(为了做)之类的事情request.path

将这些想法放在一起,Flask必须有某种方法来确定“当前”应用程序是什么也应该有意义!

您可能还具有类似于以下内容的代码:

from flask import url_for

像我们的request示例一样,该url_for函数的逻辑依赖于当前环境。但是,在这种情况下,可以清楚地看到逻辑在很大程度上取决于哪个应用程序被视为“当前”应用程序。在上面显示的前端/后端示例中,“前端”和“后端”应用程序都可能具有“ /登录”路由,因此url_for('/login')应返回不同的内容,具体取决于视图是否正在处理针对前端或后端应用程序的请求。

要回答您的问题...

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

从请求上下文文档中:

由于请求上下文在内部作为堆栈维护,因此可以多次推送和弹出。这对于实现内部重定向之类的东西非常方便。

换句话说,即使您通常在这些“当前”请求或“当前”应用程序堆栈中有0或1个项目,也可能会有更多或更多的项目。

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

这是两个单独的堆栈,还是都是一个堆栈的一部分?

它们是两个单独的堆栈。但是,这是一个实现细节。更重要的是没有堆栈,而是可以随时获取“当前”应用程序或请求(堆栈顶部)的事实。

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

“请求上下文”是“请求上下文堆栈”的一项。与“应用程序上下文”和“应用程序上下文堆栈”类似。

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

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

但是,在某些情况下,您想自己操作堆栈。

在请求之外运行代码

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

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

然后,他们在应从外壳程序运行的脚本中使用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

3
这仍然让我感到困惑!如果要进行内部重定向,为什么不拥有一个请求上下文并替换它。对我来说似乎是一个清晰的设计。
马丁(Maarten)

@Maarten如果在处理请求A时发出请求B,而请求B替换了堆栈上的请求A,则对请求A的处理无法完成。但是,即使您按照建议的方式执行了替换策略并且没有堆栈(这意味着内部重定向会更加困难),这也并没有真正改变需要应用程序和请求上下文隔离请求处理的事实。
马克·希尔德斯

很好的解释!但是我仍然有些困惑:“应用程序上下文是根据需要创建和销毁的。它永远不会在线程之间移动,也不会在请求之间共享。” 在烧瓶的文件中。为什么“应用程序上下文”不能与应用程序一起保留?
jayven,2015年

1
在Flask中使用内部重定向的示例可能会有所帮助,但对其进行的搜索并不多。如果不是那样,那么request = Local()global.py 的简单设计就足够了吗?可能有一些我没有想到的用例。
QuadrupleA

导入视图时将应用程序上下文推送到工厂方法中可以吗?因为视图包含引用current_app的路由,所以我需要上下文。
变量

48

先前的答案已经很好地概述了在请求期间Flask后台发生的情况。如果您还没有阅读它,我建议您在阅读之前先@MarkHildreth的答案。简而言之,将为每个http请求创建一个新的上下文(线程),这就是为什么必须要有一个Local允许诸如request和的g可以跨线程全局访问,同时保持它们的请求特定上下文。此外,在处理http请求时,Flask可以从内部模拟其他请求,因此有必要将它们各自的上下文存储在堆栈中。另外,Flask允许多个wsgi应用程序在单个进程中相互运行,并且在请求期间可以调用多个(每个请求都创建一个新的应用程序上下文)以执行操作,因此需要应用程序上下文堆栈。这是先前答案中所涵盖内容的摘要。

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

首先让我们了解一下Werkzeug如何实现线程Locals。

本地

当http请求进入时,将在单个线程的上下文中对其进行处理。作为在http请求期间生成新上下文的一种替代方法,Werkzeug还允许使用greenlets(一种较轻的“微线程”)代替普通线程。如果您没有安装greenlet,它将恢复为使用线程。这些线程(或Greenlet)中的每一个都可以通过唯一的ID进行标识,您可以使用模块的get_ident()功能进行检索。这个函数是出发点,以神奇的背后有requestcurrent_appurl_forg,等这样的背景下,结合全局对象。

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

现在我们有了身份函数,我们可以随时知道我们在哪个线程上,并且可以创建所谓的线程 Local的上下文对象,该对象可以全局访问,但是当您访问其属性时,它们将解析为它们的值该特定线程。例如

# globally
local = Local()

# ...

# on thread 1
local.first_name = 'John'

# ...

# on thread 2
local.first_name = 'Debbie'

这两个值同时存在于全局可访问Local对象上,但访问local.first_name在线程1的上下文中进行将为您提供'John',而'Debbie'在线程2 上将返回。

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

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()为当前的greenlet或线程。然后,Local存储仅将其用作密钥来存储与当前线程相关的任何数据。

Local每个流程和request,可以有多个对象gcurrent_app而其他对象就可以像这样简单地创建。但这不是Flask的工作方式,在技术上这些都不是 Local对象对象,而是更准确的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自己的优点是简化了它们的管理。您只需要一个Local对象即可创建许多可全局访问的代理。在请求结束时,在清理过程中,您只需释放一个Local(即,从其存储中弹出context_id),而不必理会代理,它们仍可全局访问,并且仍然依赖于代理Local来查找其对象对后续的http请求感兴趣。

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

为了简化LocalProxy已经存在的创建Local,Werkzeug实现了Local.__call__()magic方法,如下所示:

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.globals)这仍然不是如何requestgcurrent_appsession创建。正如我们已经建立的那样,Flask可以产生多个“假”请求(来自单个真实的http请求),并且在此过程中还可以推送多个应用程序上下文。这不是常见的用例,但是是框架的功能。由于这些“并发”请求和应用仍被限制为仅在任何时候都只有一个具有“焦点”的情况下运行,因此将堆栈用于其各自的上下文是有意义的。每当产生新请求或调用一个应用程序时,它们就会将上下文推入各自堆栈的顶部。Flask LocalStack为此目的使用对象。当他们结束业务时,他们将上下文弹出堆栈。

本地堆栈

这是一个LocalStack看起来像(再次代码被简化,以方便其逻辑的理解)。

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

请注意,从上面看,a LocalStack是存储在本地中的堆栈,而不是存储在堆栈中的一堆本地。这意味着尽管堆栈可以全局访问,但每个线程中的堆栈都是不同的。

瓶没有它requestcurrent_appg,和session物品直接解决的LocalStack,它,而使用LocalProxy对象包装查找功能(而不是Local对象)会发现从底层对象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()wsgi应用程序的进入点(即Web服务器调用什么并将http环境传递给何时)。请求进入),并按照创建RequestContext对象都通过其随后push()进入_request_ctx_stack。一旦推送到堆栈的顶部,就可以通过以下方式访问它_request_ctx_stack.top。以下是一些简短的代码来演示流程:

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

app = Flask(*config, **kwconfig)

# ...

后来有一个http请求进入,WSGI服务器使用通常的参数调用该应用程序。

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()

    # ...

这大致就是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.path从您的一个视图函数中查找的内容将如下所示:

  • 从全局可访问LocalProxy对象开始request
  • 要找到其感兴趣的基础对象(代理的对象),它将调用其查找功能_find_request()(注册为该功能的功能)self.local)。
  • 该函数查询LocalStack对象_request_ctx_stack的堆栈顶部上下文。
  • 为了找到顶部上下文,LocalStack对象首先在其内部Local属性(self.local)中查询stack先前存储在此处属性。
  • 来自 stack获得顶级上下文
  • top.request因此被解析为感兴趣的底层对象。
  • 从那个对象我们得到path属性

因此,我们已经了解了LocalLocalProxy和如何LocalStack工作,现在思考一下path从中检索的含义和细微差别:

  • 一个request可能是简单的全局可访问对象的对象。
  • 一个request对象,这将是一个地方。
  • 一个request对象存储为本地的属性。
  • 一个request对象,它是存储在本地对象的代理。
  • request存储在堆栈中的对象,该对象又存储在本地中。
  • 一个request对象,它是存储在本地的堆栈上的对象的代理。<-这就是Flask所做的。

4
出色的总结,我一直在研究flask / globals.py和werkzeug / local.py中的代码,这有助于阐明我的理解。我的幻想告诉我这是一种过于复杂的设计方式,但是我承认我不了解它打算用于的所有用例。我在上面的说明中看到了“内部重定向”的唯一理由,而谷歌搜索“烧瓶内部重定向”并没有引起太大的变化,因此我仍然有些茫然。我喜欢flask的一件事是,它通常不是充满AbstractProviderContextBaseFactories之类的Java对象汤类的东西。
QuadrupleA

1
@QuadrupleA一旦你了解这些LocalLocalStackLocalProxy工作,我建议重新审视DOC的这些文章:flask.pocoo.org/docs/0.11/appcontextflask.pocoo.org/docs/0.11/extensiondevflask.pocoo .org / docs / 0.11 / reqcontext。您的新鲜掌握可能会让您焕然一新,并可能提供更多见解。
Michael Ekoka

仔细阅读这些链接-它们在大多数情况下都是有意义的,但设计仍然让我感到震惊,因为它过于复杂,也许对于自己的利益而言太聪明了。但是我一般都不是OOP的忠实拥护者,也不喜欢隐式流控制(覆盖__call __(),__getattr __(),动态事件分配与简单函数调用,将内容包装在属性访问器中,而不仅仅是使用常规属性等) 。)所以也许这只是哲学上的差异。也不是TDD的从业者,这似乎是许多这种额外的机制都打算支持的。
QuadrupleA

1
感谢您的分享,不胜感激。线程是python之类的语言的弱点-您最终会遇到上述模式,这些模式会泄漏到应用程序框架中,并且也无法真正扩展。Java是另一个类似情况的例子。众所周知,threadlocals,semaphors等很难正确维护。这是诸如Erlang / Elixir(使用BEAM)之类的语言或事件循环方法(例如nginx与apache等)通常提供功能更强大,可扩展性和更简单的方法的地方。
arcseldon

13

@Mark Hildreth的答案很少。

上下文堆栈看起来像{thread.get_ident(): []},在这里[]称为“堆栈”,因为仅用于appendpushpop[-1]__getitem__(-1))操作。因此上下文堆栈将保留线程或greenlet线程的实际数据。

current_appgrequestsession和等是LocalProxy刚刚overrided特殊的方法对象__getattr____getitem____call____eq__等,并从上下文堆栈顶部(返回值[-1])的参数名(current_apprequest例如)。 LocalProxy需要一次导入此对象,并且它们不会丢失实际情况。所以最好只是导入request在代码中的任何地方,而不是将请求参数发送给您的函数和方法。您可以使用它轻松编写自己的扩展名,但不要忘记,琐碎的用法会使代码难以理解。

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

那么如何填充两个堆栈?根据要求Flask

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

2

让我们举一个例子,假设您要设置一个用户上下文(使用Local和LocalProxy的flask构造)。

定义一个User类:

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()))

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

说明:

1.Local具有identity和objet dict,identity是threadid或greenlet id,在此示例中,_local.user = User()与_local等效。___storage __ [当前线程的id] [“ user”] = User()

  1. LocalProxy 操作委托给包装好的本地对象,或者您可以提供一个返回目标对象的函数。在上面的示例中,get_user函数将当前用户对象提供给LocalProxy,当您通过usercontext.userid要求当前用户的userid时,LocalProxy的__getattr__函数首先调用get_user以获取User对象(用户),然后调用getattr(user,“ userid”)。只需在用户(在当前线程或greenlet中)上设置userid即可:usercontext.userid =“ user_123”
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.