Flask的上下文栈的目的是什么?
我一直在使用请求/应用程序的上下文一段时间没有完全理解它是如何工作的,或为什么它的devise是这样的。 当涉及到请求或应用程序上下文时,“堆栈”的目的是什么? 这两个单独的堆栈,还是它们都是一个堆栈的一部分? 请求上下文是否被压入堆栈,还是自己是堆栈? 我能够在彼此之上推送/popup多个上下文吗? 如果是这样,我为什么要这样做?
对不起所有的问题,但是在阅读请求上下文和应用上下文的文档后我仍然感到困惑。
多个应用程序
应用程序上下文(及其目的)确实令人困惑,直到你意识到Flask可以有多个应用程序。 想象一下你想要一个WSGI Python解释器运行多个Flask应用程序的情况。 我们这里不是在说Blueprints,而是在谈论完全不同的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应用程序正在创build“前端”和“后端”。 换句话说, Flask(...)
应用程序的构造函数被调用了两次,创build了Flask应用程序的两个实例。
上下文
当您使用Flask时,通常最终会使用全局variables来访问各种function。 例如,你可能有代码读取…
from flask import request
然后,在视图中,您可以使用request
来访问当前请求的信息。 显然, request
不是一个正常的全局variables, 实际上,这是一个上下文的本地价值。 换句话说,幕后有一些魔术说:“当我调用request.path
,从CURRENT请求的request
对象中获取path
属性。 两个不同的请求对于request.path
会有不同的结果。
事实上,即使你使用multithreading运行Flask,Flask也足够聪明,可以保持请求对象的隔离。 这样做,两个线程(每个处理不同的请求)就可以同时调用request.path
并为它们各自的请求获取正确的信息。
把它放在一起
所以我们已经看到Flask可以在同一个解释器中处理多个应用程序,而且由于Flask允许使用“上下文本地”全局variables的方式,所以必须有一些机制来确定“当前” 请求是什么为了做一些事情,比如request.path
)。
将这些想法放在一起,Flask必须有一些方法来确定“当前”应用程序是什么意思!
您可能也有代码类似于以下内容:
from flask import url_for
像我们的request
示例一样, url_for
函数的逻辑取决于当前的环境。 然而,在这种情况下,很明显,逻辑很大程度上取决于哪个应用程序被认为是“当前”应用程序。 在上面显示的前端/后端示例中,“前端”和“后端”应用程序都可以具有“/ login”路由,因此url_for('/login')
应该返回不同的值,具体取决于视图是否处理请求为前端或后端应用程序。
要回答你的问题
当涉及到请求或应用程序上下文时,“堆栈”的目的是什么?
从请求上下文文档:
由于请求上下文在内部作为堆栈进行维护,因此可以多次推送和popup。 这是非常方便的实现像内部redirect的东西。
换句话说,即使您通常在这些“当前”请求或“当前”应用程序堆栈中将有0个或1个项目,但您可能还有更多。
给出的例子是你的请求会返回“内部redirect”的结果。 假设用户请求A,但是您想要返回给用户B.在大多数情况下,您向用户发出redirect,并将用户指向资源B,这意味着用户将运行第二个请求来获取B.稍微不同的处理方式是做一个内部redirect,这意味着在处理A时,Flask将为资源B发出一个新的请求,并将第二个请求的结果作为用户原始请求的结果。
这两个单独的堆栈,还是它们都是一个堆栈的一部分?
他们是两个单独的堆栈 。 但是,这是一个实现细节。 更重要的不是有一个堆栈,而是在任何时候你可以获得“当前”应用程序或请求(堆栈顶部)。
请求上下文是否被压入堆栈,还是自己是堆栈?
“请求上下文”是“请求上下文堆栈”的一个项目。 与“应用上下文”和“应用上下文堆栈”类似。
我能够在彼此之上推送/popup多个上下文吗? 如果是这样,我为什么要这样做?
在Flask应用程序中,你通常不会这样做。 您可能想要进行内部redirect的一个示例(如上所述)。 即使在这种情况下,然而,你可能最终会让Flask处理一个新的请求,所以Flask会为你做所有的推送/popup。
但是,在某些情况下,您想要自己操作堆栈。
在请求之外运行代码
人们遇到的一个典型问题是,他们使用Flask-SQLAlchemy扩展来设置SQL数据库和模型定义,使用如下所示的代码:
app = Flask(__name__) db = SQLAlchemy() # Initialize the Flask-SQLAlchemy extension object db.init_app(app)
然后,他们在应该从shell运行的脚本中使用app
和db
值。 例如,一个“setup_tables.py”脚本…
from myapp import app, db # Set up models db.create_all()
在这种情况下,Flask-SQLAlchemy扩展知道app
程序app
程序,但是在create_all()
期间它会抛出一个错误,抱怨没有应用程序上下文。 这个错误是有道理的。 在运行create_all
方法时,您从来没有告诉Flask应该处理哪个应用程序。
您可能想知道为什么当您在视图中运行类似的function时,最终不需要with app.app_context()
调用。 原因是Flask在处理实际的Web请求时已经处理了应用程序上下文的pipe理。 这个问题实际上只出现在这些视图函数之外(或者其他的callback函数),比如在一次性脚本中使用模型。
解决scheme是自己推动应用程序上下文,这可以通过做…
from myapp import app, db # Set up models with app.app_context(): db.create_all()
这将推动一个新的应用程序上下文(使用应用程序的app
,记住可能有多个应用程序)。
testing
另一个你想操作栈的情况是用于testing。 您可以创build一个处理请求的unit testing,并检查结果:
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
以前的答案已经给出了在请求期间Flask背景中发生的事情的一个很好的概述。 如果你还没有阅读,我build议@ MarkHildreth的答案在阅读之前。 简而言之,为每个http请求创build一个新的上下文(线程),这就是为什么有必要有一个线程Local
设施,允许对象(如request
和g
跨线程全局访问,同时保持其请求特定的上下文。 而且,在处理一个http请求时,Flask可以模仿来自内部的附加请求,因此需要将它们各自的上下文存储在堆栈上。 此外,Flask允许多个wsgi应用程序在单个进程内相互运行,并且在请求期间可以调用多个wsgi应用程序(每个请求创build一个新的应用程序上下文),因此需要应用程序的上下文堆栈。 这是对以前答案中涵盖的内容的总结。
我现在的目标是通过解释Flask和Werkzeug如何对这些上下文当地人做什么来补充我们当前的理解。 我简化了代码以增强对其逻辑的理解,但是如果你得到这个,你应该能够很容易地掌握大部分实际源代码( werkzeug.local
和flask.globals
)。
我们先来了解Werkzeug如何实现线程本地。
本地
当http请求进入时,它将在单个线程的上下文中进行处理。 作为在http请求期间产生新上下文的替代scheme,Werkzeug还允许使用greenlet(一种较轻的“微线程”)而不是普通的线程。 如果你没有安装greenlet,它将会恢复使用线程。 这些线程(或greenlet)中的每一个都可以通过一个唯一的ID来标识,您可以使用该模块的get_ident()
函数来检索它。 该函数是request
, current_app
, url_for
, g
和其他这样的上下文绑定的全局对象背后的魔法的起点。
try: from greenlet import get_ident except ImportError: from thread import get_ident
现在我们有了我们的身份识别function,我们可以知道我们在任何给定时间处于哪个线程,我们可以创build一个线程Local
,一个可以全局访问的上下文对象,但是当您访问它的属性时,它们将parsing为它们的值为特定的线程。 例如
# 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
对象, request
g
, current_app
和其他的可以简单地创build。 但是这不是在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 ie 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 ...
现在要创build全球可访问的代理,你会做的
# this would happen some time near application start-up local = Local() request = LocalProxy(local, 'request') g = LocalProxy(local, 'g')
现在有一段时间,在请求过程的早期,您可以将一些对象存储在先前创build的代理可以访问的本地中,而不pipe我们所在的线程
# this would happen early during processing of an http request local.request = RequestContext(http_environment) local.g = SomeGeneralPurposeContainer()
使用LocalProxy
作为全局可访问对象而不是使Locals
本身成为Locals
的好处是可以简化pipe理。 您只需要一个Local
对象来创build许多全球可访问的代理。 在请求结束时,只需要释放一个Local
(即从存储中popupcontext_id),不用担心代理服务器,它们仍然可以全局访问,仍然可以按照Local
要求他们感兴趣的对象后续http请求。
# this would happen some time near the end of request processing release(local) # aka local.__release_local__()
当我们已经有一个Local
,为了简化LocalProxy
的创build,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)中寻找,那么仍然不是如何创buildrequest
, g
, current_app
和session
。 正如我们已经确定的,Flask可以产生多个“假”请求(来自单个真正的http请求),并且在这个过程中也推送多个应用程序上下文。 这不是一个常见的用例,但它是框架的一个function。 由于这些“并发”请求和应用程序仍然受限于只有一个具有“焦点”的应用程序,所以在各自的上下文中使用堆栈是有意义的。 每当产生一个新的请求或调用其中一个应用程序时,它们将其上下文推送到它们各自的堆栈顶部。 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没有request
, current_app
, g
和session
对象直接parsing到LocalStack
,而是使用LocalProxy
对象来包装一个查找函数(而不是Local
对象),它将从LocalStack
find底层对象:
_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()
看看上下文是如何被插入到堆栈中的(然后popup),请查看作为wsgi应用程序入口点的flask.app.Flask.wsgi_app()
(即web服务器当请求进来时调用并传递http环境),然后通过后续的push()
将_request_ctx_stack
对象创build为_request_ctx_stack
。 一旦推到栈顶,可以通过_request_ctx_stack.top
访问。 这里有一些简短的代码来演示stream程:
所以,你启动一个应用程序,并将其提供给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它感兴趣的底层对象(它所代理的对象),它调用它的查找函数
_find_request()
(它被注册为self.local
的函数)。 - 该函数查询
LocalStack
对象_request_ctx_stack
以查找堆栈顶部的上下文。 - 为了find顶级上下文,
LocalStack
对象首先查询其先前存储在其中的stack
属性的内部Local
属性(self.local
)。 - 从
stack
获得顶级的上下文 - 并且
top.request
因此被解决作为潜在的兴趣对象。 - 从那个对象我们得到
path
属性
所以我们已经看到了Local
, LocalProxy
和LocalStack
如何工作的,现在想一想在从以下地方获取path
的含义和细微差别:
-
request
对象将是一个简单的全局可访问的对象。 -
request
对象将是本地的。 - 作为本地属性存储的
request
对象。 - 一个
request
对象,它是存储在本地的对象的代理。 - 一个存储在堆栈中的
request
对象,它又被存储在本地。 - 一个
request
对象,它是存储在本地的堆栈上的一个对象的代理。 < – 这是Flask所做的。
@Mark Hildreth的回答很less。
上下文堆栈看起来像{thread.get_ident(): []}
,其中[]
称为“堆栈”,因为只使用append
( push
), pop
和[-1]
( __getitem__(-1)
)操作。 所以上下文栈将保持线程或greenlet线程的实际数据。
current_app
, g
, request
, session
等是LocalProxy
对象,它只是覆盖特殊方法__getattr__
, __getattr__
, __getattr__
, __getattr__
__call__
等,并通过参数名( current_app
, request
例如)从上下文栈顶( [-1]
。 LocalProxy
需要导入这个对象一次,他们不会错过现实。 所以更好的是只要你在代码中导入request
,而不是发送请求参数到你的函数和方法。 你可以很容易地用它来编写自己的扩展,但是不要忘记,轻率的使用会使代码更难以理解。
花时间去了解https://github.com/mitsuhiko/werkzeug/blob/master/werkzeug/local.py 。
那么如何填充这两个堆栈呢? 根据要求Flask
:
- 通过环境创build
request_context
(initmap_adapter
,匹配path) - input或推送此请求:
- 清除以前的
request_context
- 如果它错过并推送到应用程序上下文堆栈,则创build
app_context
- 这个请求被推送到请求上下文堆栈
- init会话,如果它错过了
- 清除以前的
- 派遣请求
- 清除请求并从堆栈popup
让我们举一个例子,假设你想设置一个usercontext(使用本地和本地代理的瓶构造)。
定义一个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中获取用户的用户ID
说明:
1.本地有一个身份和objet的字典,身份是threadid或greenlet id,在本例中_local.user = User()等价于_local .___存储__ [当前线程的id] [“user”] = User()
- LocalProxy 委托操作来包装Local对象,或者你可以提供一个返回目标对象的函数。 在上面的例子中,get_user函数将当前用户对象提供给LocalProxy,并且当您通过usercontext.userid询问当前用户的用户标识时,LocalProxy的__getattr__函数首先调用get_user来获取用户对象(user),然后调用getattr(user,“userid”)。 在用户(在当前线程或greenlet)上设置用户ID只需执行:usercontext.userid =“user_123”