Flask中上下文栈(context stacks)的目的?

[回答者Mark Hildreth]

Multiple Apps 多个应用

Flask可以有多个应用,如果没有了解到这一点,应用上下文的作用确实会令人迷惑。考虑一下这种场景:你想在一个WSGI python解释器运行多个Flask应用。这里我们讲的不是蓝本,而是完全不同的Flask应用。
一个应用分发(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与backend(前端与后端)
换句话说,通过调用两次Flask(…)的构造方法来创建两个flask应用实例

Contexts 上下文

当你使用Flask时,需要经常使用全局变量访问不同的函数。例如,你可能会读到下边的代码:

from flask import request

在某个视图中,你可能会使用request对象访问当前请求的信息。显然,request不是全局变量。实际上,它是一个context local值。这里做了一个特殊处理,使得当我们请求request.path时能正确的从当前request的对象中获取path属性。

实际上,即使在Flask中运行多个线程,Flask也能够保持request对象的隔离性。在这种情况下,不同的线程可以处理各自的请求,并发的获取request.path信息,而且确保在他们各自的请求中能获取到正确的信息。

Putting it Together 综合考虑

我们已经看到Flask可以在同一个解释器中处理多个应用,这是由于Flask允许你使用”context local”全局变量来实现的。这其中肯定是有某种机制决定究竟哪个是当前的request对象。

综合以上情况,Flask一定有某种方法判断当前的应用是哪个。
你可能会碰到如下代码:

from flask import url_for

与request例子类似,url_for函数逻辑上是取决于当前环境。在这种情形下,可以十分肯定的说这种逻辑与Flask认定哪个app是当前的app有着紧密的关系。在上边的frontend/backend例子中,frontend与backend的app中可能都包含/login路由,url_for(‘/login’)会为不同app返回不同的值,这取决于视图正在处理的request(frontend或backend)

To answer your questions… 书归正传,回答问题

当我们谈到request或者应用上下文时,栈的目的是什么?

Request Context 文档:
由于在request上下文中维护者一个栈,因此你可以多次地出栈和入栈。这种方式十分有利于实现内部的重定向转发。
换句话说,你可以将多个内部的重定向请求放入“当前”requests或“当前”应用的栈中。

下边给出一个例子,你可以使request返回“内部重定向”的结果。例如一个用户对A发起请求,并将它返回的结果传给用户B。大多数情况下,你会给这个用户生成一个重定向请求,将这个用户重新定向到资源B,这意味着用户要运行第二个请求去获取B。与此对比。略微有些不同的处理方式是使用内部重定向,当处理A时,Flask会为资源B本身生成一个新的请求,将第二个请求的结果作为用户原始请求的结果。(笔者附注:不同体现在,第一种方法是程序员自己创建第二个request请求,而第二种是Flask框架自动创建这种request,并返回正确的结果给我们)

问题:这两个栈是分别独立的还是他们是一个栈的两部分?
他们是两个独立的栈。在任何时候你可以获得“当前”的应用或请求(位于栈的顶部)。源码:

flask.globals.py
……
#contextlocals
_request_ctx_stack=LocalStack()    #request 上下文栈
_app_ctx_stack=LocalStack()   # Flask应用上下文栈

问题:request上下文是被推进栈中,还是其本身就是一个栈?
一个请求上下文是request 上下文栈(_request_ctx_stack)的元素。“应用上下文”与”app context stack”之间的关系与之类似。

问题:可以在两个栈的顶部push/pop多个上下文吗?如果可以,何种情况下需要这么做?
在Flask应用中,你通常不需要这么做。举个例子,你可能会在处理内部重定向时想要这么做。但是,即使在这种情形下,你也应该利用Flask处理新请求,Flask会为你做好所有的push/pop操作。

有一些情形下你需要自己进行栈操作

Running code outside of a request 在请求之外运行代码

一个典型的例子是 ,当使用 Flask-SQLAlchemy扩展设置SQL数据库或者定义模型时。当使用类似下方的代码时,

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

我们需要在shell中使用app和db的值时:

from myapp import app, db
# Set up models
db.create_all()

在这种情况下,Flask-SQLAlchemy扩展需要知道app应用的信息,但是当执行create_all()时,会抛出一个没有上下文的错误。

RuntimeError: application not registered on db instance and no application bound to current context

这个错误是正常的,这是由于在运行create_all()时,你没有告诉Flask这个应用需要处理的信息。
你可能疑惑,为何在视图中你运行相似的函数,没有使用with app.app_context()也没有出现错误呢。原因是,当你处理实际的web请求时,flask已经替你自动管理好应用的上下文。此类问题只会出现在:当有代码运行在视图函数(或类似的回调函数)以外的情形下。

解决方案是自己将应用上下文推入栈中,例如:

from myapp import app, db
# Set up models
with app.app_context():
    db.create_all()

测试

另一个需要手动操作栈的地方是测试。你可以创建一个单元测试用来处理请求和检查结果:

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:
            # 你可以在这查看请求上下文栈的属性
#此处请求上下文栈会是空的

[回答者mike_e]

每个http请求都要创建一个上下文(线程),这是必须创建Local线程的原因。这样可以通过维护特定的request上下文,来保证request和g这样的对象可以被全局访问。此外,Flask在处理Http请求时,可以从内部模拟request,这就必须要求在一个栈中存储他们各自的上下文。Flask允许多个WSGI应用运行在单一进程中,并且在一个request请求中可能调用不止一个应用,因此必须为应用设计一个上下文栈。
我们首先来理解一下werkzeug如何实现Local线程。
Local
当发起一个 http请求时,某个线程的上下文会处理这个请求。也就是说,在http请求发起的同时会产生一个新的上下文。Werkzeug(__version__='0.12.2') 允许使用greenlets替代python的原生线程。如果没有安装greenlets会使用threads代替。每个线程都有一个唯一id作为标志符,get_ident()函数提供线程的检索功能。这个函数隐藏在request, current_app,url_for, g,等上下文绑定的全局对象中。

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

通过线程检索函数get_ident,我们可以很容易的知道当前的线程。我们可以创建一个叫做Local的线程,Local是一个上下文对象,可以被全局的访问。你可以访问特定线程的属性值。
例如:

# globally
local = Local()
# ...
# on thread 1
local.first_name = 'John'
# ...
# on thread 2
local.first_name = 'Debbie'

在同一时刻,可以通过访问Local来获取这两个线程的属性值。当查询local.first_name时,线程1的上下文会返回’John’,而线程2会返回’Debbie’

这是如何做到的呢?我们看一下Local的源码:

werkzeug.local.py
class Local(object):
    __slots__ = ('__storage__', '__ident_func__')

    def __init__(self):
        # 初始化线程字典__storage__,键:线程id,值:线程
        object.__setattr__(self, '__storage__', {})
        #get_ident 函数生成线程的Id
        object.__setattr__(self, '__ident_func__', get_ident)

     def __release_local__(self):
        self.__storage__.pop(self.__ident_func__(), None)

    def __getattr__(self, name):
        try:
            return self.__storage__[self.__ident_func__()][name]
        except KeyError:
            raise AttributeError(name)

    def __setattr__(self, name, value):
        ident = self.__ident_func__()
        storage = self.__storage__
        try:
            storage[ident][name] = value   #更新属性值
        except KeyError:
            storage[ident] = {name: value} # 设置属性值

从上面的代码可以看到,Local使用字典storage存储线程和相应的线程id.
键:线程id,值:线程。初始化时绑定字典和线程id生成函数。getattr是从字典中根据id取出线程。 setattr将特定的线程放入字典中(或者更新已有的值)。在Flask中并没有使用Local对象,使用的是LocalProxy 对象。

LocalProxy
class LocalProxy(object):
    def __init__(self, local, name):
       # local这里是一个实际的Local对象,可以用来查找特定的对象,其标识符是name.
       #local是可以调用的,可以确定代理对象
       self.local = local
       # 'name'作为标识符,传递给local来查找特定的对象
       self.name = name

    def _get_current_object(self):
        #如果self.local是一个Local对象,则其已经实现了__release_local__()方法,
        #正如其名字一样,通常用来释放Local对象
        #这里通过简单的查找来标记哪个是实际的Local对象,哪个是可调用的对象
        if hasattr(self.local, '__release_local__'):
        try:
            return getattr(self.local, self.name)
        except AttributeError:
            raise RuntimeError('no object bound to %s' % self.name)

        #如果self.local不是一个Local对象,则一定是可调用的对象
        #,这样可以决定用户感兴趣的对象
        return self.local(self.name)

    #现在LocalProxy 执行其特定的职责
    #比如,在Local中代理一个对象,我们将感兴趣的对象的魔幻方法
    #全部交给代理来处理
    @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
    # ...  等等 ... 
    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]
    # ... 等等 ...
    __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
# ...  等等 …

现在你可以这样创建全局访问代理对象
# this would happen some time near application start-up
local = Local()
request = LocalProxy(local, 'request')
g = LocalProxy(local, 'g')

在request请求的前期,你可以在local(之前创建的代理)中存储一些对象,无论哪个线程都可以访问:

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

使用LocalProxy 为全局访问对象,相对直接使用Local对象而言,可以简化对象管理。你可以为一个单一的Local对象创建许多全局代理对象。在request请求的末期(清理阶段),你可以简单的释放一个Local(对其storage执行pop操作),并且这种操作不会影响代理,这些代理对象仍然可以全局的访问,并处理随后的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.py),你仍然无法知道request, g, current_app 和session等对象时如何创建。当我们创建应用时,Flask会同时创建多个”假的”request请求(从一个真正的http请求)并且在过程中push多个应用上下文。由于这些”并发”的请求和应用在任一时刻只有一个被处理。因此,有必要使用一个栈来储存他们各自的上下文。当一个新的请求产生或者一个应用被调用时,他们会将上下文push进各自的栈中。Flask使用LocalStack目的正是如此。当他们结束自己的业务时会从栈中弹出上下文。

LocalStack

class LocalStack(object):
    def __init__(self):
        self.local = Local()
    def push(self, obj):
        """压入栈中一个新的元素"""
        rv = getattr(self.local, 'stack', None)
        if rv is None:
            self.local.stack = rv = []
        rv.append(obj)
        return rv
    def pop(self):
        """移除栈顶的元素, 返回旧值或None.
            """
        stack = getattr(self.local, 'stack', None)
        if stack is None:
            return None
        elif len(stack) == 1:
            release_local(self.local) # 释放 local
            return stack[-1]
        else:
            return stack.pop()
    @property
    def top(self):
        """T获取栈顶的元素 """
        try:
            return self.local.stack[-1]
        except (AttributeError, IndexError):
            return None

注意,LocalStack是一个驻存在Local对象里的栈,并不是存储Local对象的栈。这意味着,尽管这个栈是全局可以访问的,但是其在彼此的线程中是不同的。
Flask并没有直接的从LocalStack中获取request, current_app, g, 和session等对象,而是包装了一层查找功能来寻找潜在的对象。

class LocalStack(object):
    …..
    """__version__='0.12.2'"""
    def __call__(self):
        def _lookup():
            rv = self.top
            if rv is None:
                raise RuntimeError('object unbound')
            return rv
        return LocalProxy(_lookup)
    …

#flask.globals.py   [__version__='0.12.2']

def _lookup_req_object(name):   #在request上下文栈中查找属性
    top = _request_ctx_stack.top
    if top is None:
        raise RuntimeError(_request_ctx_err_msg)
    return getattr(top, name)


def _lookup_app_object(name):  #在应用上下文栈中查找属性
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return getattr(top, name)


def _find_app():  #在应用上下文栈中查找当前的应用
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return top.app


# context locals
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))
g = LocalProxy(partial(_lookup_app_object, 'g'))

所有的上述对象,在应用建立的开始,栈中不会有任何的对象。除非你将一个request或者应用的上下文压入到他们各自的栈中。

如果你对上下文是如何插入栈中的细节感兴趣,可以参考源码flask.app.Flask.wsgi_app()。当登录到wsgi的应用时web服务器将http环境参数传递给request请求,随后创建RequestContext 对象。然后调用push()方法将上下文压入_request_ctx_stack栈中。一旦push到栈的顶部,就可以全局的访问_request_ctx_stack.top。这里列出上述流程的部分代码:
建立一个WSGI应用:

app = Flask(*config, **kwconfig)
# ...

随后http请求到达服务器,WSGI服务器调用app:

app(environ, start_response) # aka app.__call__(environ, start_response)
app中大致的处理过程如下:
class 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 请求完成初始化后,接着在视图函数功能中查找request.path。随后的处理步骤如下:

• 流程的起点是全局可访问的LocalProxy 对象
• 在_lookup_req_object函数中查找特定的对象
• _lookup_req_object函数在_request_ctx_stack栈的顶部查找对象
• 为了查找顶部的上下文,LocalStack 对象首先检索内部的Local属性(self._local),并设置Localstack属性
• 从stack中获取顶top的上下文
• top.request可以决定客户端感兴趣的对象
• 从这个对象中获取path属性

我们已经知道Local, LocalProxy, 和 LocalStack的工作机制,现在总结一下:
request 对象是一个简单的全局可访问的对象。它是一个代理对象,储存在Local对象的属性stack中,即存储在stack的栈顶。


参考文献:

[1]https://stackoverflow.com/questions/20036520/what-is-the-purpose-of-flasks-context-stacks/20041823#20041823

<think>好的,我现在需要帮用户解答Flask中应用上下文和请求上下文的区别。首先,我得回顾一下相关的知识,确保自己理解正确。根据用户提供的引用资料,Flask有两种上下文:应用上下文和请求上下文。这两者的区别可能涉及它们的定义、生命周期、存储的数据以及使用场景。 首先,应用上下文。根据引用[2]和引用[5],应用上下文(application context)与Flask应用实例相关。它存储的是应用级别的配置信息,比如数据库连接、应用配置等。应用上下文在应用启动时被创建,并在整个应用运行期间存在,但可能因为请求的处理而被暂时推入或弹出。引用[3]提到应用上下文用于缓存资源,比如数据库连接,而且因为应用上下文被应用和扩展共享,所以需要唯一的变量名。 然后是请求上下文。引用[1]和[4]指出,请求上下文(request context)处理的是HTTP请求相关的数据,如request对象和session。每次客户端发起请求时,服务器会创建请求上下文,请求处理完毕后就会被销毁。请求上下文的生命周期较短,仅限于单次请求期间。例如,request对象封装了客户端请求的信息,而session用于保持用户会话状态。 接下来需要比较两者的区别。生命周期是一个关键点:应用上下文的生命周期比请求上下文长,一个应用存活期间可能有多个请求,每个请求都有自己的上下文。存储的数据类型也不同,应用上下文保存应用级别的配置和资源,而请求上下文保存请求相关的数据。此外,它们的创建和销毁时机不同,应用上下文可能在应用启动时或需要时创建,而请求上下文在每个请求到来时创建,请求结束后销毁。 还要注意全局变量如current_app和g属于应用上下文,而request和session属于请求上下文。这点在引用[4]和[5]中都有提到。g变量虽然存在于应用上下文中,但它是每次请求都会重置的临时存储,这可能容易让人混淆,需要明确说明。 用户可能想知道为什么需要这两种上下文,以及它们如何协同工作。例如,应用上下文允许在请求之外访问应用配置,比如在命令行或后台任务中使用current_app。而请求上下文确保每个请求的数据隔离,避免信息泄露。 可能还需要解释Flask上下文机制,应用上下文和请求上下文都是通过来管理的,这样可以在需要时推入和弹出,尤其是在处理多个应用或请求时,如测试或使用多个应用实例。 最后,总结它们的区别,确保结构清晰,分点说明。同时根据用户提供的引用资料,正确标注引用来源,比如应用上下文的共享资源和请求上下文的每次请求独立。 在生成相关问题的时候,可以考虑用户可能进一步想知道如何使用这些上下文,或者具体例子,比如如何在视图函数外访问上下文,或者如何处理多线程中的上下文问题。</think>### Flask应用上下文与请求上下文的区别 #### 1. **定义与核心作用** - **应用上下文** 存储与**Flask应用实例**相关的全局配置和资源,例如数据库连接、应用密钥等。通过变量`current_app`(当前应用实例)和`g`(请求期间的临时存储)体现[^3][^4][^5]。 **生命周期**:与应用实例共存,但在实际运行中可能被多次推入/弹出上下文(例如后台任务或命令行操作时)[^2][^5]。 - **请求上下文** 封装**单次HTTP请求**的交互数据,例如客户端请求内容(`request`对象)和用户会话(`session`字典)。 **生命周期**:仅存在于一次HTTP请求从接收到响应的过程中,请求结束后自动销毁[^1][^4]。 #### 2. **生命周期对比** | 上下文类型 | 创建时机 | 销毁时机 | |----------------|----------------------------|--------------------------| | 应用上下文 | 应用启动或首次请求处理时 | 应用关闭或手动移除时 | | 请求上下文 | 每次HTTP请求到达时 | HTTP请求处理完成后 | #### 3. **典型应用场景** - **应用上下文** - 在非请求场景(如CLI命令)中访问应用配置[^2]。 - 跨请求共享资源(如数据库连接池)[^3]。 ```python from flask import current_app # 在视图函数外获取应用配置 db_url = current_app.config['DATABASE_URI'] ``` - **请求上下文** - 获取客户端请求参数(如`request.args`)[^1]。 - 维护用户会话状态(如`session['user_id']`)[^4]。 ```python from flask import request @app.route('/login', methods=['POST']) def login(): username = request.form['username'] # 从请求上下文中获取数据 session['user'] = username # 操作请求上下文中的session ``` #### 4. **全局变量与数据隔离** - **应用上下文变量** - `current_app`:始终指向当前活动的应用实例[^2]。 - `g`:单次请求内的临时存储,每次请求重置(尽管属于应用上下文,但其数据与请求绑定)[^4]。 - **请求上下文变量** - `request`:包含HTTP请求的URL参数、表单数据等[^1]。 - `session`:跨请求的用户会话存储(依赖客户端Cookie加密)[^4]。 #### 5. **设计原理与协作** Flask通过**上下文**管理这两种上下文: - 请求到来时,**请求上下文**和**应用上下文**会被依次推入中[^2]。 - 视图函数通过顶自动获取当前上下文(如`request.url`实际调用顶的请求上下文)。 - 多应用或多请求场景(如测试时)可通过手动管理上下文实现隔离[^3]。 --- 相关问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值