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

我已经使用请求/应用程序上下文有一段时间了,但是没有完全理解它是如何工作的,也没有完全理解为什么它是这样设计的。当涉及到请求或应用程序上下文时,“堆栈”的目的是什么?这是两个独立的堆栈,还是它们都是一个堆栈的一部分?请求上下文是推送到堆栈上,还是堆栈本身?我是否能够将多个上下文相互推送或弹出?如果是这样,我为什么要这么做?

很抱歉提出这些问题,但是在阅读了请求上下文和应用程序上下文的文档之后,我仍然感到困惑。

54827 次浏览

多个应用程序

应用程序上下文(及其用途)确实令人困惑,直到您意识到 Flask 可以有多个应用程序。想象一下这样一种情况: 您希望一个 WSGI Python 解释器运行多个 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.path时,从 CURRENT 请求的 request对象中获取 path属性。”对于 request.path,两个不同的请求将产生不同的结果。

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

把它放在一起

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

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

您可能还有类似于下面的代码:

from flask import url_for

像我们的 request示例一样,url_for函数具有依赖于当前环境的逻辑。然而,在这种情况下,很明显,逻辑严重依赖于哪个应用程序被认为是“当前”应用程序。在上面显示的前端/后端示例中,“前端”和“后端”应用程序都可以有一个“/login”路由,因此 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)

然后,它们在应该从 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

小加 @ Mark Hildreth的答案。

上下文堆栈类似于 {thread.get_ident(): []},其中 []称为“堆栈”,因为它只使用 append(push)、 pop[-1](__getitem__(-1))操作。因此,上下文堆栈将保留线程或 greenlet 线程的实际数据。

current_appgrequestsession等是 LocalProxy对象,它只覆盖了特殊方法 __getattr____getitem____call____eq__等,并通过参数名称(例如 current_apprequest)从上下文堆栈顶部([-1])返回值。 LocalProxy需要导入这个对象一次,它们不会错过实际情况。所以最好只导入代码中的 request,而不是向函数和方法发送请求参数。您可以很容易地用它编写自己的扩展,但是不要忘记,轻率的使用会使代码更难理解。

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

那么这两个堆栈是如何填充的呢:

  1. 通过环境创建 request_context(init map_adapter,match path)
  2. 输入或按下这个请求:
    1. 清除之前的 request_context
    2. 如果遗漏并推送到应用程序上下文堆栈,则创建 app_context
    3. 将此请求推送到请求上下文堆栈
    4. 如果错过了初始化会话
  3. 派遣请求
  4. 清除请求并从堆栈中弹出

以前的答案已经很好地概述了在请求过程中 Flask 的背景中发生了什么。如果你还没有读过,我推荐@MarkHildreth 在读这篇文章之前给出答案。简而言之,为每个 http 请求创建一个新的上下文(线程) ,这就是为什么需要一个线程 Local工具,它允许像 requestg这样的对象可以跨线程全局访问,同时维护它们的请求特定的上下文。此外,在处理 http 请求 Flask 时,可以模拟来自内部的其他请求,因此需要将它们各自的上下文存储在堆栈中。此外,Flask 允许多个 wsgi 应用程序在单个进程中相互运行,并且在一个请求期间可以调用多个应用程序来执行操作(每个请求创建一个新的应用程序上下文) ,因此应用程序需要一个上下文堆栈。这是之前答案的总结。

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

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

本地

当一个 http 请求进入时,将在单个线程的上下文中处理该请求。作为在 http 请求期间产生新上下文的一种替代方法,Werkzeug 还允许使用 greenlet (一种较轻的“微线程”)代替普通线程。如果您没有安装 greenlet,它将恢复到使用线程代替。每个线程(或 greenlet)都可以通过一个惟一的 id 来识别,您可以使用模块的 get_ident()函数来检索这个 id。这个函数是拥有 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对象上,但是在线程1的上下文中访问 local.first_name将得到 'John',而在线程2上返回 'Debbie'

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

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对象,可以像这样简单地创建 requestgcurrent_app和其他对象。但是在 Flask 中不是这样做的,在 Flask 中这些不是 严格来说 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本身的优点是,它简化了它们的管理。您只需要一个 Local对象就可以创建许多全局可访问的代理。在请求结束时,在清理过程中,您只需释放一个 Local(即从其存储中弹出 context _ id) ,并且不需要使用代理,它们仍然是全局可访问的,并且仍然遵从一个 Local来为后续的 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) ,仍然不知道 requestgcurrent_appsession是如何创建的。正如我们已经建立的,Flask 可以产生多个“假”请求(来自一个真正的 http 请求) ,并且在这个过程中还推送多个应用程序上下文。这不是一个常见的用例,但它是一个框架的能力。由于这些“并发”请求和应用程序仍然受到限制,在任何时候只能运行一个具有“焦点”的请求和应用程序,因此对它们各自的上下文使用堆栈是有意义的。无论何时产生新的请求或调用其中一个应用程序,它们都会将上下文推到各自堆栈的顶部。Flask 为此使用 LocalStack对象。当他们完成他们的业务,他们弹出堆栈的上下文。

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

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

Flask 没有它的 requestcurrent_appgsession对象直接解析为 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属性

因此,我们已经看到了 LocalLocalProxyLocalStack是如何工作的,现在我们来思考一下从以下几个方面检索 path的含义和细微差别:

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

让我们举一个例子,假设您想设置一个 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. 本地人有身份和客体的定义。身份是一个线性的或绿色的 ID。在这个例子中,_local.user = User()等价于 _local.___storage__[current thread's id] ["user"] = User()

  2. LocalProxy LocalProxy2操作来包装 Local 对象,或者您可以提供一个返回目标对象的函数。在上面的例子中,get_user函数向 LocalProxy提供当前用户对象,当你通过 usercontext.userid请求当前用户的 userid时,LocalProxy__getattr__函数首先调用 get_user来获取 User对象(用户) ,然后调用 getattr(user,"userid")。要在 User上设置 userid(在当前线程或 greenlet 中) ,只需执行以下操作: LocalProxy1