Flask中的上下文

前言

上一篇中我们已经知道flask运行的大体流程(Flask的工作原理),其中进入wsgi_app中首先创建的就是上下文环境,那么什么是上下文呢,又有什么作用。在了解上下文之前,先要弄清楚LocalProxy,Local,LocalStack这三个概念。

Local

根据werkzeug文档介绍,local是提供了线程隔离的数据访问方式,类似于python中的 thread locals。可以理解为存在一个全局的数据,不同线程可以读取和修改它。不同线程之间是彼此隔离的。

什么是线程隔离呢?

比如存在一个全局变量数字10,在线程1中把10改为1,主线程中读取这个数字,发现数字变成了1,也就是说新线程数据影响了主线程数据。这样一来,多线程之间考虑其他线程带来的影响,从而不能安全地读取和修改数据。

import threading


class A:
    a = 10


obj = A()


def worker1():
    """线程1"""
    obj.a = 1


t1 = threading.Thread(target=worker1, name='线程1')
t1.start()
t1.join()
print(obj.a)

结果

1

为什么不使用python thread local呢?因为这个有一些缺陷,比如

  • 有些应用使用的是greenlet协程,这种情况下无法保证协程之间数据的隔离,因为不同的协程可以在同一个线程当中。
  • 即使使用的是线程,WSGI应用也无法保证每个http请求使用的都是不同的线程,因为后一个http请求可能使用的是之前的http请求的线程,这样的话存储于thread local中的数据可能是之前残留的数据。

而Local解决了上面的问题,实现了线程之间的数据隔离,从而能够安全的读取和修改数据。

werkzeug中Local的实现

class Local(object):
    __slots__ = ("__storage__", "__ident_func__")

    def __init__(self):
        object.__setattr__(self, "__storage__", {})
        object.__setattr__(self, "__ident_func__", get_ident)

    def __iter__(self):
        return iter(self.__storage__.items())

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

    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}

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

local的实现方式是使用python中的dict结构,根据每个线程的id去创建独立的数据,当访问数据时,根据当前线程的id进行访问,每个线程访问自己的数据即可。这样就实现了线程隔离的数据访问。

Local引入了get_ident方法用于获取当前线程的id,将get_ident方法存储至类的__ident_func__属性中,将所有线程的数据存储至__storage__属性中,此属性对应的是一个二维dict,每个线程使用一个dict存储数据,而每个线程的dict数据作为__storage__的线程id对应的值。因此线程访问数据事实上是访问__storage__[ident][name],其中前面的ident为线程的id,后面name才是用户指定的数据key。而ident是Local自动获取的,用户可以透明进行线程隔离的数据访问与存储。

Local使用

我们可以单独使用Local,实现线程隔离。

import threading
from werkzeug.local import Local


obj = Local()
obj.a = 10


def worker1():
    """线程1"""
    obj.a = 1
    print('线程1中的a的值为:{}'.format(obj.a))


t1 = threading.Thread(target=worker1, name='线程1')
t1.start()
t1.join()
print('主线程中的a的值为:{}'.format(obj.a))

结果

线程1中的a的值为:1
主线程中的a的值为:10

obj是一个线程隔离的对象,所以线程1的改变没有影响到主线程。

LocalStack

LocalStack是一个多线程隔离栈结构,通过源码发现是基于Local实现的,提供了栈结构push,pop,top方法。

class LocalStack(object):

    def __init__(self):
        self._local = Local()

    def __release_local__(self):
        self._local.__release_local__()
    
   #  获取线程对应的id,直接复用Local的__ident_func__,即使用get_ident获取线程id
    @property
    def __ident_func__(self):
        return self._local.__ident_func__
    
    @__ident_func__.setter
    def __ident_func__(self, value):
        object.__setattr__(self._local, "__ident_func__", value)

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

        return LocalProxy(_lookup)
    # 入栈
    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)
            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使用

LockStack也可以单独使用,可以理解为就是普通的栈结构,只是这个栈结构是数据安全的。

import threading
from werkzeug.local import LocalStack


ls = LocalStack()
ls.push(1)


def worker1():
    """线程1"""
    print('线程1中的栈顶的值为:{}'.format(ls.top))
    ls.push(2)
    print('线程1中的栈顶的值为:{}'.format(ls.top))


t1 = threading.Thread(target=worker1, name='线程1')
t1.start()
t1.join()
print('主线程中的栈顶的值为:{}'.format(ls.top))

结果

线程1中的栈顶的值为:None
线程1中的栈顶的值为:2
主线程中的栈顶的值为:1

LocalProxy

LocalProxy是用来代理Local和LocalStack对象的。

class LocalProxy(object):

    __slots__ = ("__local", "__dict__", "__name__", "__wrapped__")

    def __init__(self, local, name=None):
        object.__setattr__(self, "_LocalProxy__local", local)
        object.__setattr__(self, "__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):
        """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__"):
            return self.__local()
        try:
            return getattr(self.__local, self.__name__)
        except AttributeError:
            raise RuntimeError("no object bound to %s" % self.__name__)

可以看到LocalProxy通过是_get_current_object来获取代理的对象,然后执行相应的操作即可。对proxy执行的任意操作,都是直接通过被代理对象执行的。为了保证代理对象可以直接进行操作,LocalProxy重载了所有的基本方法,这样就可以随意对proxy对象执行操作。
那么为什么需要LocalProxy来代理Local或LocalStack对象呢?

LocalProxy的作用

下面看一个例子,直接使用LocalStack。

from werkzeug.local import LocalStack
user_stack = LocalStack()
user_stack.push({'name': 'Bob'})
user_stack.push({'name': 'John'})


def get_user():
    return user_stack.pop()


# 直接调用函数获取user对象
user = get_user()
print(user['name'])
print(user['name'])

结果

John
John

使用LocalProxy

from werkzeug.local import LocalStack, LocalProxy
user_stack = LocalStack()
user_stack.push({'name': 'Bob'})
user_stack.push({'name': 'John'})


def get_user():
    return user_stack.pop()


# 使用 LocalProxy
user = LocalProxy(get_user)
print(user['name'])
print(user['name'])

结果

John
Bob

结果显而易见,直接使用LocalStack对象,user一旦赋值就无法再动态更新了,而使用Proxy,每次调用操作符,都会重新获取user,从而实现了动态更新user的效果。

而在_get_current_object方法中可以看到,对于可执行对象或方法,就是直接执行获取可执行对象或方法对应的返回值。而对于Local对象,则是获取name作为属性的值。但是要注意的是,所有的获取都是在执行操作的时候获取的,这样就可以随着程序运行动态更新。同样也可以解释了上面的例子中,为方法get_user创建的LocalProxy类型的proxy_user,可以两次执行proxy_user[‘name’]获取到不同值了,因为两次执行时,都会通过_get_current_object执行get_user方法,两次执行的结果不同,返回的值也就不同了。

了解了Local,LocalProxy,LocalStack,接下来看一下上下文。

什么是上下文

上下文多用于文章中,代表的一个整体环境,比如一篇文章,我们可以说下文中,访问到下文所陈述的内容,也可以说上文中,访问到上文中的内容,而我们这篇文章中每一段文字所代表的意思,都是要根据我们的上下文来决定的,因为你随便拿出来一句话不去结合整体的语境去理解出来的意思肯定不是准确的,所以,我们这篇文章的上下文就是我们整篇的中心思想。

这是文章中的上下文,那么程序中的上下文是什么?

程序中的上下文代表了程序当下所运行的环境,存储了一些程序运行的信息。比如在程序中我们所写的函数大都不是单独完整的,在使用一个函数完成自身功能的时候,很可能需要同其他的部分进行交互,需要其他外部环境变量的支持,上下文就是给外部环境的变量赋值,使函数能正确运行。

Flask中的上下文

flask中有两个上下文,请求上下文和应用上下文。

请求上下文

在 flask 中,可以直接在视图函数中使用 request 这个对象进行获取相关数据,而 request 就是请求上下文的对象,保存了当前本次请求的相关数据。请求上下文对象有:request、session。

  • request
    封装了HTTP请求的内容,针对的是http请求。举例:user = request.args.get('user'),获取的是get请求的参数。
  • session
    用来记录请求会话中的信息,针对的是用户信息。举例:session['name'] = user.id,可以记录用户信息。还可以通过session.get('name')获取用户信息。

应用上下文

flask 应用程序运行过程中,保存的一些配置信息,比如程序名、数据库连接、应用信息等。应用上下文对象有:current_app,g

  • current_app
    应用程序上下文,用于存储应用程序中的变量,可以通过current_app.name打印当前app的名称,也可以在current_app中存储一些变量,例如:
    应用的启动脚本是哪个文件,启动时指定了哪些参数
    加载了哪些配置文件,导入了哪些配置
    连了哪个数据库
    有哪些public的工具类、常量
    应用跑再哪个机器上,IP多少,内存多大
  • g变量
    g 作为 flask 程序全局的一个临时变量,充当者中间媒介的作用,我们可以通过它传递一些数据,g 保存的是当前请求的全局变量,不同的请求会有不同的全局变量。

current_app 的生命周期最长,只要当前程序实例还在运行,都不会失效。
request 和 g 的生命周期为一次请求期间,当请求处理完成后,生命周期也就完结了。

curren_app,g,request,session都是线程隔离的,我们可通过源码发现。

上下文的定义

Flask上下文定义在globals.py上

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 = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, "request"))
session = LocalProxy(partial(_lookup_req_object, "session"))
g = LocalProxy(partial(_lookup_app_object, "g"))

这里定义了两个LocalStack栈,_request_ctx_stack是请求上下文栈,_app_ctx_stack是应用上下文栈,curren_app,g,request,session都是LocalStack栈顶元素。

上下文处理流程

在上一篇中(Flask的工作原理)我们看到wsgi_app中会创建上下文环境,调用 ctx.push() 函数将上下文信息压栈。

 def wsgi_app(self, environ, start_response):
        ctx = self.request_context(environ)
        error = None
        try:
            try:
                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)

ctx = self.request_context(environ)实际上是实例化一个RequestContext对象。

class RequestContext(object):

    def __init__(self, app, environ, request=None, session=None):
        self.app = app
        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
        self.flashes = None
        self.session = session

    def push(self):
        top = _request_ctx_stack.top
        if top is not None and top.preserved:
            top.pop(top._preserved_exc)

        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)

        if hasattr(sys, "exc_clear"):
            sys.exc_clear()

        _request_ctx_stack.push(self)

 
        if self.session is None:
            session_interface = self.app.session_interface
            self.session = session_interface.open_session(self.app, self.request)

            if self.session is None:
                self.session = session_interface.make_null_session(self.app)

        if self.url_adapter is not None:
            self.match_request()

    def pop(self, exc=_sentinel):
       

    def auto_pop(self, exc):


    def __enter__(self):
        self.push()
        return self

    def __exit__(self, exc_type, exc_value, tb):
        self.auto_pop(exc_value)

        if BROKEN_PYPY_CTXMGR_EXIT and exc_type is not None:
            reraise(exc_type, exc_value, tb)

ctx.push()是创建上下文环境,把该请求的 ApplicationContext 和 RequestContext 有关的信息保存到对应的栈上。

到了这里Flask上下文基本明确了,每次有请求过来的时候,flask 会先创建当前线程或者进程需要处理的两个重要上下文对象,把它们保存到隔离的栈里面,这样视图函数进行处理的时候就能直接从栈上获取这些信息。

结合上篇文章,Flask整个流程就很明确了。


一个线程同时只处理一个请求,那么 _req_ctx_stack和 _app_ctx_stack肯定都是只有一个栈顶元素的,为什么还要栈这种数据结构?
每个请求同时拥有这两个上下文信息,为什么要把 request context 和 application context 分开?

为什么要使用栈这种数据结构

因为Flask可以有多个应用,也就是在一个 Python 进程中,可以拥有多个应用,如果是多个 app,那么栈顶存放的是当前活跃的 request,也就是说使用栈是为了获取当前的活跃 request 对象。

为什么要拆分请求上下文和应用上下文

为了灵活度,为了可以让我们单独创建两个上下文,以便应付不同的场景。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343