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

Ben*_*vis 145 python flask

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

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

Mar*_*eth 219

多个应用

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

您可以将此设置类似于"Application Dispatching"示例中的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
})
Run Code Online (Sandbox Code Playgroud)

请注意,创建了两个完全不同的Flask应用程序"frontend"和"backend".换句话说,Flask(...)应用程序构造函数已被调用两次,创建了Flask应用程序的两个实例.

上下文

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

from flask import request
Run Code Online (Sandbox Code Playgroud)

然后,在视图中,您可能会request用来访问当前请求的信息.显然,request这不是一个正常的全局变量; 实际上,它是一个上下文本地值.换句话说,幕后有一些魔术说:"当我打电话时request.path,pathrequestCURRENT请求的对象中获取属性." 两个不同的请求会有不同的结果request.path.

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

把它放在一起

所以我们已经看到Flask可以在同一个解释器中处理多个应用程序,而且由于Flask允许你使用"context local"全局变量的方式,必须有一些机制来确定"当前" 请求是什么(为了做这样的事情request.path).

把这些想法放在一起,Flask必须有一些方法来确定"当前"的应用程序是什么意思!

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

from flask import url_for
Run Code Online (Sandbox Code Playgroud)

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

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

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

从请求上下文文档:

由于请求上下文在内部维护为堆栈,因此您可以多次推送和弹出.这对于实现内部重定向等内容非常方便.

换句话说,即使您在这些"当前"请求或"当前"应用程序堆栈上通常只有0或1个项目,但您可能拥有更多项目.

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

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

它们是两个独立的堆栈.但是,这是一个实现细节.更重要的不是存在堆栈,而是随时可以获得"当前"应用程序或请求(堆栈顶部)的事实.

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

"请求上下文"是"请求上下文堆栈"中的一项.与"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)
Run Code Online (Sandbox Code Playgroud)

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

from myapp import app, db

# Set up models
db.create_all()
Run Code Online (Sandbox Code Playgroud)

在这种情况下,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()
Run Code Online (Sandbox Code Playgroud)

这将推送一个新的应用程序上下文(使用应用程序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
Run Code Online (Sandbox Code Playgroud)

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

Mic*_*oka 40

以前的答案已经很好地概述了在请求期间Flask背景中发生的事情.如果您还没有阅读,我会在阅读之前推荐@MarkHildreth的答案.总之,新的上下文(线程)为每个HTTP请求创建,这就是为什么它是需要有一个线程Local功能,允许物体,如requestg以跨线程全局访问,同时保持它们的请求的特定环境.此外,在处理http请求时,Flask可以模拟来自内部的其他请求,因此需要将它们各自的上下文存储在堆栈中.此外,Flask允许多个wsgi应用程序在单个进程中相互运行,并且在请求期间可以调用多个操作(每个请求创建一个新的应用程序上下文),因此需要为应用程序提供上下文堆栈.这是对以前答案所涵盖内容的总结.

现在我的目标是通过解释来补充我们目前的了解如何瓶和WERKZEUG做自己与这些背景当地人.我简化了代码以增强对其逻辑的理解,但是如果你得到这个,你应该能够轻松掌握实际源代码中的大部分内容(werkzeug.localflask.globals).

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

本地

当http请求进入时,它将在单个线程的上下文中处理.作为在http请求期间生成新上下文的另一种方法,Werkzeug还允许使用greenlet(一种较轻的"微线程")而不是普通线程.如果您没有安装greenlet,它将恢复使用线程.这些线程(或greenlet)中的每一个都可以通过唯一ID进行标识,您可以使用模块的get_ident()功能检索该ID .这功能是起点,有背后的魔力request,current_app,url_for,g,等这样的背景下,结合全局对象.

try:
    from greenlet import get_ident
except ImportError:
    from thread import get_ident
Run Code Online (Sandbox Code Playgroud)

现在我们已经拥有了我们的身份函数,我们可以知道我们在任何给定时间处于哪个线程,并且我们可以创建所谓的线程Local,一个可以全局访问的上下文对象,但是当您访问其属性时,它们会解析为那个特定的线程.例如

# globally
local = Local()

# ...

# on thread 1
local.first_name = 'John'

# ...

# on thread 2
local.first_name = 'Debbie'
Run Code Online (Sandbox Code Playgroud)

这两个值同时出现在全局可访问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()
Run Code Online (Sandbox Code Playgroud)

从上面的代码中我们可以看到神奇归结为get_ident()标识当前greenlet或线程的魔法.然后,Local存储仅将其用作将任何数据上下文存储到当前线程的密钥.

你可以有多个Local每个进程的对象和request,g,current_app和其他人可以简单地被这样产生.但这并不是它在Flask中的表现,它不是技术上的 Local对象,而是更准确的LocalProxy对象.什么是LocalProxy

LocalProxy

LocalProxy是一个查询a 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 ...
Run Code Online (Sandbox Code Playgroud)

现在,您可以创建全局可访问的代理

# this would happen some time near application start-up
local = Local()
request = LocalProxy(local, 'request')
g = LocalProxy(local, 'g')
Run Code Online (Sandbox Code Playgroud)

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

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

使用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__()
Run Code Online (Sandbox Code Playgroud)

为了简化LocalProxy我们已经拥有的时间Local,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')
Run Code Online (Sandbox Code Playgroud)

但是,如果你在烧瓶中源(flask.globals)看这仍然不是如何request,g,current_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
Run Code Online (Sandbox Code Playgroud)

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

瓶没有它request,current_app,g,和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)
Run Code Online (Sandbox Code Playgroud)

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

如果你很想知道如何在堆栈中实际插入一个上下文(并随后弹出),请查看flask.app.Flask.wsgi_app()wsgi应用程序的入口点(即Web服务器调用的内容并将http环境传递给请求进入),并按照创建RequestContext对象通过其随后所有push()进入_request_ctx_stack.一旦被推到堆栈顶部,就可以通过它访问_request_ctx_stack.top.这是一些用于演示流程的缩写代码:

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

app = Flask(*config, **kwconfig)

# ...
Run Code Online (Sandbox Code Playgroud)

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

app(environ, start_response) # aka app.__call__(environ, start_response)
Run Code Online (Sandbox Code Playgroud)

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

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

    # ...
Run Code Online (Sandbox Code Playgroud)

这与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()
Run Code Online (Sandbox Code Playgroud)

假设请求已完成初始化,因此request.path从您的一个视图函数中查找将如下所示:

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

因此,我们已经看到了如何Local,LocalProxy并且LocalStack工作,现在想一想检索path来自的含义和细微差别:

  • 一个request对象,它是一个简单的全局可访问对象.
  • request一个本地的对象.
  • 一个request对象存储为本地的属性.
  • 一个request对象,它是存储在本地的对象的代理.
  • request存储在堆栈上的对象,然后存储在本地.
  • 一个request对象,它是存储在本地的堆栈上的对象的代理.< - 这就是Flask的作用.

  • @QuadrupleA 一旦您了解了这些“Local”、“LocalStack”和“LocalProxy”的工作原理,我建议重新访问文档中的这些文章:http://flask.pocoo.org/docs/0.11/appcontext/,http: //flask.pocoo.org/docs/0.11/extensiondev/,和http://flask.pocoo.org/docs/0.11/reqcontext/。你的新认识可能会让你以新的眼光看待它们,并可能提供更多的见解。 (3认同)
  • 优秀的破旧,我一直在研究flask/globals.py和werkzeug/local.py中的代码,这有助于澄清我对它的理解.我的蜘蛛侠意识告诉我这是一种过于复杂的设计方式,但我承认我并不了解它的所有用例."内部重定向"是我在上面的描述中看到的唯一理由,并且谷歌搜索"烧瓶内部重定向"并没有出现太多,所以我仍然有点不知所措.我喜欢烧瓶的一个原因是它通常不是一个充满AbstractProviderContextBaseFactories等的java对象汤类型的东西. (2认同)

tbi*_*icr 13

@Mark Hildreth的回答很少.

上下文堆栈的样子{thread.get_ident(): []},这里[]所谓的"堆栈",因为只用append(push)pop[-1](__getitem__(-1))操作.因此上下文堆栈将保留线程或greenlet线程的实际数据.

current_app,g,request,session和等是LocalProxy对象刚刚overrided特殊的方法__getattr__,__getitem__,__call__,__eq__从上下文堆栈顶部(等和返回值[-1]通过参数名)( current_app,request例如). 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. 清除请求并从堆栈弹出它