Flask源码篇:彻底吃透Flask上下文原理

源码系列:

Flask源码篇:wsgi、Werkzeug与Flask启动工作流程
Flask源码篇:Flask路由规则与请求匹配过程

在说Flask中的上下文时,我们需要讨论以下几个主题。

了解ThreadLocal

ThreadLocal是基于线程隔离的全局对象,每个线程可以有自己的变量,互不干扰。

关于ThreadLocal的详细信息请参考下文:

https://blog.csdn.net/qq_43745578/article/details/129369272?spm=1001.2014.3001.5501

Local类

Local类位于Flask的werkzeug包下的local文件中。其源码如下:

class Local:
    __slots__ = ("_storage",)

    def __init__(self) -> None:
        object.__setattr__(self, "_storage", ContextVar("local_storage"))

    @property
    def __storage__(self) -> t.Dict[str, t.Any]:
        warnings.warn(
            "'__storage__' is deprecated and will be removed in Werkzeug 2.1.",
            DeprecationWarning,
            stacklevel=2,
        )
        return self._storage.get({
    
    })  # type: ignore

    @property
    def __ident_func__(self) -> t.Callable[[], int]:
        warnings.warn(
            "'__ident_func__' is deprecated and will be removed in"
            " Werkzeug 2.1. It should not be used in Python 3.7+.",
            DeprecationWarning,
            stacklevel=2,
        )
        return _get_ident  # type: ignore

    @__ident_func__.setter
    def __ident_func__(self, func: t.Callable[[], int]) -> None:
        warnings.warn(
            "'__ident_func__' is deprecated and will be removed in"
            " Werkzeug 2.1. Setting it no longer has any effect.",
            DeprecationWarning,
            stacklevel=2,
        )

    def __iter__(self) -> t.Iterator[t.Tuple[int, t.Any]]:
        return iter(self._storage.get({
    
    }).items())

    def __call__(self, proxy: str) -> "LocalProxy":
        """Create a proxy for a name."""
        return LocalProxy(self, proxy)

    def __release_local__(self) -> None:
        __release_local__(self._storage)

    def __getattr__(self, name: str) -> t.Any:
        values = self._storage.get({
    
    })
        try:
            return values[name]
        except KeyError:
            raise AttributeError(name) from None

    def __setattr__(self, name: str, value: t.Any) -> None:
        values = self._storage.get({
    
    }).copy()
        values[name] = value
        self._storage.set(values)

    def __delattr__(self, name: str) -> None:
        values = self._storage.get({
    
    }).copy()
        try:
            del values[name]
            self._storage.set(values)
        except KeyError:
            raise AttributeError(name) from None

(1)分析__init__方法

首先看下Local类的初始化__init__方法,代码如下:

def __init__(self) -> None:
        object.__setattr__(self, "_storage", ContextVar("local_storage"))

初始化方法只是给他设置一个单下划线属性_storage,属性值对象是ContextVar

其源码如下:

class ContextVar:  # type: ignore
    """A fake ContextVar based on the previous greenlet/threading
    ident function. Used on Python 3.6, eventlet, and old versions
    of gevent.
    """

    def __init__(self, _name: str) -> None:
        self.storage: t.Dict[int, t.Dict[str, t.Any]] = {
    
    }

    def get(self, default: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]:
        return self.storage.get(_get_ident(), default)

    def set(self, value: t.Dict[str, t.Any]) -> None:
        self.storage[_get_ident()] = value

可以看到ContextVar类的对象就getset两个方法,把线程的变量维护到self.storage变量里,self.storage就是个字典。

前面我们说过,ThreadLocal会以线程id作为键,再存线程的变量。这里类似的是,getset方法都是从``storage属性获取信息,且根据_get_ident()`方法返回值作为键的。

这个_get_ident()方法就是相当于获取当前线程id的。只不过这里更加高级一点,看local文件最上面,找到_get_ident()导入,代码如下:

try:
    from greenlet import getcurrent as _get_ident
except ImportError:
    from threading import get_ident as _get_ident


def get_ident() -> int:
    warnings.warn(
        "'get_ident' is deprecated and will be removed in Werkzeug"
        " 2.1. Use 'greenlet.getcurrent' or 'threading.get_ident' for"
        " previous behavior.",
        DeprecationWarning,
        stacklevel=2,
    )
    return _get_ident()  # type: ignore

可以看到local文件从greenlet模块先导入 _get_ident方法,然后找不到再使用线程的,所以Local高就级在不仅可以使用在线程里,还可以使用在协程里。

2)分析属性设置、获取和删除方法

根据源码看到Local类实现了三个方法用于属性的设置(__setattr__)、获取(__getattr__)和删除(__delattr__)。

而且注意,上面三个方法,其实是调用self._storage也就是ContextVar对象的getset方法来完成数据维护的。

LocalStack类

LocalStack也位于位于Flask的werkzeug包下的local文件中,是上面Local与栈的结合。源码如下:

class LocalStack:
    def __init__(self) -> None:
        self._local = Local()

    def __release_local__(self) -> None:
        self._local.__release_local__()

    @property
    def __ident_func__(self) -> t.Callable[[], int]:
        return self._local.__ident_func__

    @__ident_func__.setter
    def __ident_func__(self, value: t.Callable[[], int]) -> None:
        object.__setattr__(self._local, "__ident_func__", value)

    def __call__(self) -> "LocalProxy":
        def _lookup() -> t.Any:
            rv = self.top
            if rv is None:
                raise RuntimeError("object unbound")
            return rv

        return LocalProxy(_lookup)

    def push(self, obj: t.Any) -> t.List[t.Any]:
        """Pushes a new item to the stack"""
        rv = getattr(self._local, "stack", []).copy()
        rv.append(obj)
        self._local.stack = rv
        return rv  # type: ignore

    def pop(self) -> t.Any:
        """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)
            return stack[-1]
        else:
            return stack.pop()

    @property
    def top(self) -> t.Any:
        """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

其中有个私有变量self._local = Local()维护了一个Local对象。

还有3个和栈有关的方法:push、pop、top。可以看到LocalStack类在self._local里维护了一个属性stack,并默认为第一个空列表。然后push、pop、top三个方法分别是从列表顶部压入一项、从列表顶部取出一项、获取列表顶部的一项,列表里还在。

所以实际上LocalStack对象内部维护的self._local数据结构类似如下:

self._local = {
    
    
  "2491238714": {
    
      # 线程获取协程id
       "stack": []  # 栈信息
  }
}

最重要的是LocalStack使用Local来维护一个栈(键的名字为stack),所以这个栈还是线程或协程隔离的!也就是使用LocalStack类能够给每个线程或协程维护一个自己的栈!

LocalProxy类

在了解LocalProxy之前,我们可以先简单了解下偏函数:

偏函数实际是使用模块functools的partial方法实现的。其主要使用场景是,给一个函数预设参数,看如下例子:

from functools import partial


def my_func(name, age):
    print(f'my_func:name-{
      
      name},age-{
      
      age}')


new_func = partial(my_func, 'ethan')
print(new_func)
# functools.partial(<function my_func at 0x7fb9c00e8040>, 'ethan')

new_func(18)
# my_func:name-ethan,age-18

可以看到,使用partial函数时,第一个参数是一个函数名或者更广点说是可调用对象,第二个参数时给传进去的函数预设一个参数,上例中给my_func的name参数预设了ethan这个值,以后再调用new_func时,name永远都是ethan了。

了解偏函数后,再来看下LocalProxy

LocalProxy主要用于代理Local对象或LocalStack对象,负责把所有对自己的操作转发给内部的 LocalLocalStack对象。

在上面Local类和LocalStack类的源码中,我们可以看到,两个类都有__call__方法,且都返回了LocalProxy对象。

代码如下:

def __call__(self, proxy: str) -> "LocalProxy":
        """Create a proxy for a name."""
        return LocalProxy(self, proxy)

其中第一个参数是Local对象,第二个是要代理的名字。

LocalProxy主要源码如下(有删减):

class LocalProxy:
    """A proxy to the object bound to a :class:`Local`.

    .. code-block:: python

        from werkzeug.local import Local
        l = Local()

        # a proxy to whatever l.user is set to
        user = l("user")

        from werkzeug.local import LocalStack
        _request_stack = LocalStack()

        # a proxy to _request_stack.top
        request = _request_stack()

        # a proxy to the session attribute of the request proxy
        session = LocalProxy(lambda: request.session)

    :param local: The :class:`Local` or callable that provides the
        proxied object.
    :param name: The attribute name to look up on a :class:`Local`. Not
        used if a callable is given.
    """

    __slots__ = ("__local", "__name", "__wrapped__")

    def __init__(
        self,
        local: t.Union["Local", t.Callable[[], t.Any]],
        name: t.Optional[str] = None,
    ) -> None:
        object.__setattr__(self, "_LocalProxy__local", local)
        object.__setattr__(self, "_LocalProxy__name", name)

        if callable(local) and not hasattr(local, "__release_local__"):
            # "local" is a callable that is not an instance of Local or
            # LocalManager: mark it as a wrapped function.
            object.__setattr__(self, "__wrapped__", local)

    def _get_current_object(self) -> t.Any:
        """Return the current object.  This is useful if you want the real
        object behind the proxy at a time for performance reasons or because
        you want to pass the object into a different context.
        """
        if not hasattr(self.__local, "__release_local__"):  # type: ignore
            return self.__local()  # type: ignore

        try:
            # 这里主要是从local对象里拿药代理的对象数据
            return getattr(self.__local, self.__name)  # type: ignore
        except AttributeError:
            name = self.__name  # type: ignore
            raise RuntimeError(f"no object bound to {
      
      name}") from None
    
    # 以下是一些操作符定义,都转为_ProxyLookup对象了
    __str__ = _ProxyLookup(str)  # type: ignore
    __bytes__ = _ProxyLookup(bytes)
    __format__ = _ProxyLookup()  # type: ignore

其中初始化的方法中通过object.__setattr__()方法给当前对象设置了两个双下划线的属性_LocalProxy__local_LocalProxy__name,分别是Local对象和要代理的名字。其实这两个属性是给当前对象设置私有属性,用于在类内部使用。

LocalProxy 的构造函数有一个 callable 的参数,这个 callable 调用之后需要返回一个 Local 实例,后续所有的属性操作都会转发给 callable 返回的对象。

LocalProxy_get_current_object() 方法获取当前线程或者协程对应的self._local的要代理的对象。主要代码是return getattr(self.__local, self.__name)

源码最下面定义了一些上下划线属性,其中使用到了类_ProxyLookup,这个类就不再展开,他的主要作用是当对LocalProxy对象进行操作时,先去调用LocalProxy对象的_get_current_object() 方法,从而获取代理对象,再对dialing对象执行相应的操作。

从上面看LocalProxy貌似与偏函数没有关系,那么我们看下面一个例子:

from werkzeug.local import LocalStack, LocalProxy
from functools import partial


def my_proxy(name):
    rv = ls.top
    return rv[name]


# 创建一个LocalStack对象
ls = LocalStack()
# 调用push方法,则会在内部变量self._local里维护我们的数据
ls.push({
    
    "request": {
    
    "method": "GET", "path": "/"}})

# 创建一个LocalProxy对象,传了一个偏函数,设置name参数为request,则表示这个代理对象要代理request
# 执行my_proxy函数,并且name参数为request,则从LocalStack对象pop方法获取栈最顶的数据,再获取request
l_proxy = LocalProxy(partial(my_proxy, "request"))

# 看一下我们的代理对象
print(l_proxy)
# {'method': 'GET', 'path': '/'}
# 最后我们的代理对象就成了如上一个字典,那么直接对代理对象的操作就是代替对原对象的操作
print(l_proxy.get("method"))
# GET

通过注释可以很清楚知道,LocalProxy与偏函数结合使用,实现了对LocalProxy对象的操作代替对要代理对象操作。

而Flask中的上下文中,例如请求上下文,其实就是把上面push的内容由字典替换为了我们Request对象。下面详细讲解。

上下文globals文件

flask.globals模块中提前定义了几个全局变量,就是Flask里的上下文对象,源码如下:

import typing as t
from functools import partial

from werkzeug.local import LocalProxy
from werkzeug.local import LocalStack

if t.TYPE_CHECKING:
    from .app import Flask
    from .ctx import _AppCtxGlobals
    from .sessions import SessionMixin
    from .wrappers import Request

_request_ctx_err_msg = """\
Working outside of request context.

This typically means that you attempted to use functionality that needed
an active HTTP request.  Consult the documentation on testing for
information about how to avoid this problem.\
"""
_app_ctx_err_msg = """\
Working outside of application context.

This typically means that you attempted to use functionality that needed
to interface with the current application object in some way. To solve
this, set up an application context with app.app_context().  See the
documentation for more information.\
"""


def _lookup_req_object(name):
    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: "Flask" = LocalProxy(_find_app)  # type: ignore
request: "Request" = LocalProxy(partial(_lookup_req_object, "request"))  # type: ignore
session: "SessionMixin" = LocalProxy(  # type: ignore
    partial(_lookup_req_object, "session")
)
g: "_AppCtxGlobals" = LocalProxy(partial(_lookup_app_object, "g"))  # type: ignore

首先初始化了两个对象_request_ctx_stack_app_ctx_stack,两个都是LocalStack对象,前面讲过LocalStack对象就是栈与Local的结合,可以用来存储不同线程的变量,互不干扰。

然后就是通过LocalProxy和偏函数结合创建了4个代理对象:

  1. current_app:通过函数_find_app代理了_app_ctx_stack.top对象;
  2. request:通过函数_lookup_req_object代理了_request_ctx_stack.toprequest对象;
  3. session:通过函数_lookup_req_object代理了_request_ctx_stack.topsession对象;
  4. g:通过函数_lookup_app_object代理了_app_ctx_stack.topg对象;

这4个的创建和我们上一节的例子很像,都是通过LocalProxy来代理想要的对象,完成线程间的变量隔离。

其中requestsession属于请求上下文,和请求相关,current_appg属于应用上下文,和Flask app应用相关。

请求上下文

现在再回头来看看请求上下文究竟是什么。

在 flask 中,视图函数需要知道它执行情况的请求信息(请求的 url,参数,方法等)以及应用信息(应用中初始化的数据库等),才能够正确运行。

最直观地做法是把这些信息封装成一个对象,作为参数传递给视图函数。但是这样的话,所有的视图函数都需要添加对应的参数,即使该函数内部并没有使用到它。

flask 的做法是把这些信息作为类似全局变量的东西,视图函数需要的时候,可以使用 from flask import request 获取。但是这些对象和全局变量不同的是——它们必须是动态的,因为在多线程或者多协程的情况下,每个线程或者协程获取的都是自己独特的对象,不会互相干扰。

这就是我们的LocalLocalStackLocalProxy索要协作完成的事了。

现在再回头看下我们请求的流程,可回顾下文章:Flask源码篇:Flask路由规则与请求匹配过程

请求流程和上下的关系,主要体现在如下几步:

(1)wsgi_app

前面文章说到,请求进来时,会经过Falsk对象的wsgi_app方法,再来看下源码。

其中只有本次分析相关的主要代码:

class Flask(Scaffold):
  	# 有几个类变量
    # 请求对象类
  	request_class = Request
    # 响应对象类
    response_class = Response
    # 配置类
    config_class = Config
    # 上篇文章分析过Rule类
    url_rule_class = Rule
    # 上篇文章分析过 Map类
    url_map_class = Map
    
    def __init__(
        self,
        import_name: str,
        static_url_path: t.Optional[str] = None,
        static_folder: t.Optional[t.Union[str, os.PathLike]] = "static",
        static_host: t.Optional[str] = None,
        host_matching: bool = False,
        subdomain_matching: bool = False,
        template_folder: t.Optional[str] = "templates",
        instance_path: t.Optional[str] = None,
        instance_relative_config: bool = False,
        root_path: t.Optional[str] = None,
    ):
    		# 有几个成员变量
        # 这里放被before_first_request装饰器装饰的函数
        self.before_first_request_funcs: t.List[BeforeFirstRequestCallable] = []
        
        # 初始化Map对象
        self.url_map = self.url_map_class()
        
    def wsgi_app(self, environ: dict, start_response: t.Callable) -> t.Any:
        # 这里初始化了一个RequestContext对象,上一章也分析过这个类
        ctx = self.request_context(environ)
        error: t.Optional[BaseException] = None
        try:
            try:
                # 调用了push方法
                ctx.push()
                # 执行请求分配,拿到请求
                response = self.full_dispatch_request()
            except Exception as e:
                error = e
                response = self.handle_exception(e)
            except:  # noqa: B001
                error = sys.exc_info()[1]
                raise
            return response(environ, start_response)
        finally:
          	# 这里一定会执行的方法
            if self.should_ignore_error(error):
                error = None
            # 在执行完请求,拿到响应对象后,这个方法是把栈给自动清空了,防止栈内存过大溢出
            ctx.auto_pop(error)
    
    def request_context(self, environ: dict) -> RequestContext:
        return RequestContext(self, environ)
      
    
    def full_dispatch_request(self) -> Response:
        self.try_trigger_before_first_request_functions()
        try:
            request_started.send(self)
            rv = self.preprocess_request()
            if rv is None:
                rv = self.dispatch_request()
        except Exception as e:
            rv = self.handle_user_exception(e)
        return self.finalize_request(rv)
    
    def dispatch_request(self) -> ResponseReturnValue:
        req = _request_ctx_stack.top.request
        if req.routing_exception is not None:
            self.raise_routing_exception(req)
        rule = req.url_rule
        if (
            getattr(rule, "provide_automatic_options", False)
            and req.method == "OPTIONS"
        ):
            return self.make_default_options_response()
        return self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args)
     

看到代码首先获取了一个RequestContext对象,然后执行了它的push方法。下面再来具体分析下。

(2)ctx.push()

下面再来仔细分析下这个方法做了什么和上下文有关的事:

class RequestContext:
  	def __init__(
        self,
        app: "Flask",
        environ: dict,
        request: t.Optional["Request"] = None,
        session: t.Optional["SessionMixin"] = None,
    ) -> None:
      	# Flask应用对象
      	self.app = app
        # 这里是初始化request对象,调用了app.request_class方法,由上面的代码Flask源码看到就是实例化了Request对象
        if request is None:
            request = app.request_class(environ)
        self.request = request
        # 初始化路由分配对象
        self.url_adapter = None
        try:
            self.url_adapter = app.create_url_adapter(self.request)
        except HTTPException as e:
            self.request.routing_exception = e
        # 初始化session对象
        self.session = session
    
    def push(self) -> None:
        # _request_ctx_stack局势我们的全局上下文变量(LocalStack对象)
        # 先从栈顶拿一个元素
        top = _request_ctx_stack.top
        if top is not None and top.preserved:
            top.pop(top._preserved_exc)

        # 在把请求上下文压栈之前,必须保证有应用上下文,没有的话会初始化一个并执行其push方法
        app_ctx = _app_ctx_stack.top
        if app_ctx is None or app_ctx.app != self.app:
            app_ctx = self.app.app_context()
            app_ctx.push()
            self._implicit_app_ctx_stack.append(app_ctx)
        else:
            self._implicit_app_ctx_stack.append(None)

        # 执行请求上下文的push方法,传的参数是RequestContext对象自己,把自己压入到_request_ctx_stack的栈中了
        _request_ctx_stack.push(self)

				# 匹配请求,找到rule对象和参数对象并放到请求上下文的request属性中
        if self.url_adapter is not None:
            self.match_request()

其中有一步就是 _request_ctx_stack.push(self):执行请求上下文的push方法,传的参数是RequestContext对象自己,把自己压入到_request_ctx_stack的栈中了。

(3)app.full_dispatch_request

这个方法是在ctx.push()后执行,其代码如下:

def full_dispatch_request(self) -> Response:
    self.try_trigger_before_first_request_functions()
    try:
        request_started.send(self)
        rv = self.preprocess_request()
        if rv is None:
           # 执行dispatch_request方法
            rv = self.dispatch_request()
    except Exception as e:
        rv = self.handle_user_exception(e)
    return self.finalize_request(rv)

其中最主要的是执行self.dispatch_request()方法拿到响应。其代码如下。

(4)app.dispatch_request

def dispatch_request(self) -> ResponseReturnValue:
    # 从请求下文中拿到Request对象
    req = _request_ctx_stack.top.request
    if req.routing_exception is not None:
        self.raise_routing_exception(req)
    # 从请求下文中拿到Rule对象,这也是在ctx.push时放进请求下文中的
    rule = req.url_rule
    if (
        getattr(rule, "provide_automatic_options", False)
        and req.method == "OPTIONS"
    ):
        return self.make_default_options_response()
    # 找到匹配的路由函数并执行,返回结果
    return self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args)

这个方法主要做了如下几件事:

  1. 从请求下文中拿到Request对象;
  2. 从请求下文中拿到Rule对象(这也是在ctx.push时放进请求下文中的);
  3. 找到匹配的路由函数并执行,返回结果。

所以请求上下文在我们的路由匹配中也有非常重要的作用。

(5)使用请求上下文

如下例子,在视图函数中使用请求上下文:

from flask import Flask, request

app = Flask(__name__)


@app.route('/')
def hello_world():
    # 当导入我们的request对象,就可以拿到当前请求线程的变量了
    print(request)
    return 'Hello World!'


if __name__ == '__main__':
    app.run('127.0.0.1', 9000)

当再视图函数中导入我们的request对象,就可以拿到当前请求线程的变量了。注意这里request是Flask在globals文件里定义全局变量,用来代理请求对象的,在上面已经讲过。

request: "Request" = LocalProxy(partial(_lookup_req_object, "request")) 

(6)ctx.auto_pop(error)

wsgi方法的最后,当请求处理完拿到响应后,调用这个方法,把栈 的内容删除,用来防止栈数据过大溢出。

应用上下文

(1)源码解析

除了上面讲的请求上下文,还有一个应用上下文application context

应用上下文主要用来确定请求所在的应用,并存储应用程序中的变量。

在Flask全局变量文件globals文件里定义两个对象 _app_ctx_stack及其代理对象current_app。当个还有g对象,单独下一节讲。

与请求上下文类似,应用上下文也是利用LocalLocalProxy来完成数据的压栈和出栈。

值得注意的是,应用上下文是request context中的一个对 app 的代理。它的作用主要是帮助 request 获取当前的应用。Flask中将应用相关的信息单独拿出来,形成一个“应用上下文”对象。这个对象可以和“请求上下文”一起使用,也可以单独拿出来使用。

还记得上面讲请求上下时,说过在把请求上下文压栈之前,必须保证有应用上下文,没有的话会初始化一个并执行其push方法。代码如下:

class RequestContext:
  	def push(self) -> None:
      	# 省略前面的...
        # 这里在push之前保证有应用上下文,没有的话会初始化一个并执行其`push`方法
        app_ctx = _app_ctx_stack.top
        if app_ctx is None or app_ctx.app != self.app:
            # 初始化一个app_ctx
            app_ctx = self.app.app_context()
            app_ctx.push()
            self._implicit_app_ctx_stack.append(app_ctx)
        else:
            self._implicit_app_ctx_stack.append(None)
       # 执行请求上下文栈的push方法
        _request_ctx_stack.push(self)

其中self.app.app_context()方法用于返回一个应用上下文对象,代码如下:

def app_context(self) -> AppContext:
    return AppContext(self)

AppContext源码如下:

class AppContext:
    """The application context binds an application object implicitly
    to the current thread or greenlet, similar to how the
    :class:`RequestContext` binds request information.  The application
    context is also implicitly created if a request context is created
    but the application is not on top of the individual application
    context.
    """

    def __init__(self, app: "Flask") -> None:
        self.app = app
        self.url_adapter = app.create_url_adapter(None)
        self.g = app.app_ctx_globals_class()

        # Like request context, app contexts can be pushed multiple times
        # but there a basic "refcount" is enough to track them.
        self._refcnt = 0

    def push(self) -> None:
        """Binds the app context to the current context."""
        self._refcnt += 1
        _app_ctx_stack.push(self)
        appcontext_pushed.send(self.app)

    def pop(self, exc: t.Optional[BaseException] = _sentinel) -> None:  # type: ignore
        """Pops the app context."""
        try:
            self._refcnt -= 1
            if self._refcnt <= 0:
                if exc is _sentinel:
                    exc = sys.exc_info()[1]
                self.app.do_teardown_appcontext(exc)
        finally:
            rv = _app_ctx_stack.pop()
        assert rv is self, f"Popped wrong app context.  ({
      
      rv!r} instead of {
      
      self!r})"
        appcontext_popped.send(self.app)

    def __enter__(self) -> "AppContext":
        self.push()
        return self

    def __exit__(
        self, exc_type: type, exc_value: BaseException, tb: TracebackType
    ) -> None:
        self.pop(exc_value)

由以上代码可以看出:“应用上下文”也是一个上下文对象,它也实现了pushpop等方法。

self.g是一个全局的存储值的对象。接着将这个应用上下文也存放到了LocalStack()

应用上下文的构造函数也和请求上下文类似,都有appurl_adapter等属性。

(2)应用上下文使用

通过调用_request_ctx_stack.top.app或者current_app也可以获得当前上下文环境正在处于哪个应用。

也可以使用with语句构造一个上下文环境,with后自动执行push方法,结束后自动执行pop方法:

from flask import Flask
from flask.globals import current_app


app = Flask(__name__)

print(current_app)
print(current_app.name)


if __name__ == '__main__':
    app.run(debug=True)

启动程序,发现程序可以启动成功,但是在执行with语句里代码时,会报一个错误:

RuntimeError: Working outside of application context.

This typically means that you attempted to use functionality that needed
to interface with the current application object in some way. To solve
this, set up an application context with app.app_context().  See the
documentation for more information.

这是因为当前主线程中应用上下文还没有被push入栈。那为什么我们在视图函数里可以直接使用呢?如下:

from flask import Flask
from flask.globals import current_app


app = Flask(__name__)

@app.route('/')
def hello_world():
    print(current_app)
    return 'Hello World!'


if __name__ == '__main__':
    app.run(debug=True)

这就是因为我们上面分析的:在请求上下文的push之前会先检查有没有应用上下文,没有会帮我们自动把应用上下文初始化并调用push方法。

那在我们没有请求环境,比如在离线文件中,可以有如下两种方法来解决上面的问题:

(1)手动调用push方法

from flask import Flask
from flask.globals import current_app


app = Flask(__name__)
app.app_context().push()

print(current_app)  # <Flask 'test'>:当前的app对象
print(current_app.name)  # test:app的名字


if __name__ == '__main__':
    app.run(debug=True)

(2)使用with语句

with后自动执行push方法帮我们入栈,结束后自动执行pop方法:

from flask import Flask
from flask.globals import current_app


app = Flask(__name__)

with app.app_context():
    print(current_app)  # <Flask 'test'>
    print(current_app.name)  # test


if __name__ == '__main__':
    app.run(debug=True)

(3)为什么需要应用上下文

还有个问题是,既然已经有请求上下文了,为什么还需要应用上下文呢?

这是因为Flask支持多应用,即多个app。为了实现app隔离,Flask使用LocalStack数据结构来记录每个app的上下文。

当在一个应用的请求上下文环境中,需要嵌套处理另一个应用的相关操作时,如何让请求找到“正确”的应用呢?

如下:

首先建立第一个app:

from flask import Flask
from flask.globals import current_app


app = Flask(__name__)

app.mysql_conn = "mysql1"


if __name__ == '__main__':
    app.run(debug=True)

启动应用。

然后建立第二个app:

from flask import Flask
from flask.globals import current_app
from app_ctx_test1 import app


app2 = Flask("app2")

app2.mysql_conn = "mysql2"


@app2.route('/')
def hello_world():
    print(current_app)
    print(current_app.mysql_conn)
    with app.app_context():
        print(current_app)
        print(current_app.mysql_conn)
    return 'Hello World!'


if __name__ == '__main__':
    app2.run(debug=True, port=8000)

启动后,请求进来打印如下:

<Flask 'app2'>  # 第二个应用
mysql2  # 第二个应用的mysql信息
<Flask 'app_ctx_test1'>  #  # 第一个应用
mysql1  # 第一个应用的mysql信息

可以看到在第二个app中可以通过current_app拿到第一个app的信息,这就是做到了app的信息隔离。

所以使用应用上下文是有必要的。

g对象

g对象也是应用上下文的一种。通过上面的应用上下文源码可以看到,这是一个Flask.app_ctx_globals_class默认 为的实例 ctx._AppCtxGlobals

def _lookup_app_object(name):
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return getattr(top, name)
  
g: "_AppCtxGlobals" = LocalProxy(partial(_lookup_app_object, "g")) 

可以看到,g对象是用来代理_app_ctx_stack里的应用上下文app的g属性的。

g对象一般用来请求期间存储资源,充当中间媒介的作用,通过它在一次请求调用的多个函数间传递一些数据。每次请求都会重设这个变量。

from flask import Flask, g
 
app = Flask(__name__)
 
 
def db_query():
    user_id = g.user_id
    user_name = g.user_name
    print('user_id={} user_name={}'.format(user_id, user_name))
 
 
@app.route('/')
def get_user_profile():
    g.user_id = 123
    g.user_name = 'itcast'
    db_query()
    return 'hello world'
 
 
app.run()

参考:

https://cizixs.com/2017/01/13/flask-insight-context/

https://www.cnblogs.com/hq82/p/10451483.html

https://zhuanlan.zhihu.com/p/26097310

https://www.jianshu.com/p/7a7efbb7205f

猜你喜欢

转载自blog.csdn.net/qq_43745578/article/details/129574039