Flask上下文机制

预备知识:

本地线程

我们要在同一个进程中隔离不同线程的数据,那么我们会优先选择threading.local,来实现数据的彼此隔离.但是当基于协程(Greenlet/Eventlet)实现时,Local很难满足我们的需求.
在Flask中基于Werkzeug实现了可以满足协程的Local,werkzeug.local.Local
werkzeug.local.Local源码如下

class Local(object):
    #__slots__是为了限制此类的可绑定属性,对于其派生类不起作用.
    __slots__ = ('__storage__', '__ident_func__')

    # 当该类实例化时调用object的方法给Local对象绑定上__storage__和__ident_func__属性或方法.
    # 为啥使用自己的__setattr__方法?因为这样会进入一个死循环,自己写一下代码试试吧
    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)

其中通过get_ident这个方法来拿到线程或者协程的唯一标识

try:
    from greenlet import getcurrent as get_ident
except ImportError:
    try:
        from thread import get_ident
    except ImportError:
        from _thread import get_ident

通过上面的引用代码来看.Werkzeug实现的Local与threading.local最大的不同点是:

werkzeug会在Greenlet可用的情况下优先使Greenlet的id而不是本地线程

LocalStack和LocalProxy

LocalStack是基于Local实现的一个栈结构.栈的特性就是后入先出。当我们进入一个 Context 时,将当前的的对象推入栈中。然后我们也可以获取到栈顶元素。从而获取到当前的上下文信息。
LocalStack的代码如下:

class LocalStack(object):

    """This class works similar to a :class:`Local` but keeps a stack
    of objects instead.  This is best explained with an example::

        >>> ls = LocalStack()
        >>> ls.push(42)
        >>> ls.top
        42
        >>> ls.push(23)
        >>> ls.top
        23
        >>> ls.pop()
        23
        >>> ls.top
        42

    They can be force released by using a :class:`LocalManager` or with
    the :func:`release_local` function but the correct way is to pop the
    item from the stack after using.  When the stack is empty it will
    no longer be bound to the current context (and as such released).

    By calling the stack without arguments it returns a proxy that resolves to
    the topmost item on the stack.

    .. versionadded:: 0.6.1
    """

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

    def __release_local__(self):
        self._local.__release_local__()

    def _get__ident_func__(self):
        return self._local.__ident_func__

    def _set__ident_func__(self, value):
        object.__setattr__(self._local, '__ident_func__', value)
    __ident_func__ = property(_get__ident_func__, _set__ident_func__)
    del _get__ident_func__, _set__ident_func__

    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

LocalProxy 是代理模式的一种实现。在实例化的时候,传入一个 callable 的参数。然后这个参数被调用后将会返回一个 Local 对象。我们后续的所有操作,比如属性调用,数值计算等,都会转发到这个参数返回的 Local 对象上。
现在大家可能不太清楚,我们为什么要用 LocalProxy 来进行操作,我们来给大家看一个例子

from werkzeug.local import LocalStack
test_stack = LocalStack()
test_stack.push({'abc': '123'})
test_stack.push({'abc': '1234'})

def get_item():
    return test_stack.pop()

item = get_item()

print(item['abc'])
print(item['abc'])

你看我们这里的输出的的值,都是统一的 1234 ,但是我们这里想做到的是每次获取的值都是栈顶的最新的元素,那么我们这个时候就应该用 proxy 模式了

from werkzeug.local import LocalStack, LocalProxy
test_stack = LocalStack()
test_stack.push({'abc': '123'})
test_stack.push({'abc': '1234'})

def get_item():
    return test_stack.pop()

item = LocalProxy(get_item)
print(item['abc'])
print(item['abc'])

你看我们这里就是 Proxy 的妙用。
在Flask中常用全局变量如下,他们良好的利用了LocalStack和LocalProxy

#global.py文件
# 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'))

Contex

由于 Flask 基于 Werkzeug 实现,因此 App Context 以及 Request Context 是基于前文中所说的 LocalStack 实现。

从命名上,大家应该可以看出,App Context 是代表应用上下文,可能包含各种配置信息,比如日志配置,数据库配置等。而 Request Context 代表一个请求上下文,我们可以获取到当前请求中的各种信息。比如 body 携带的信息。

这两个上下文的定义是在 flask.ctx 文件中,分别是 AppContext 以及 RequestContext 。而构建上下文的操作则是将其推入在 flask.globals 文件中定义的 _app_ctx_stack 以及 _request_ctx_stack 中。前面说了 LocalStack 是“线程”(这里可能是传统意义上的线程,也有可能是 Greenlet 这种)隔离的。同时 Flask 每个线程只处理一个请求,因此可以做到请求隔离。

目前还有一个小疑问:
1.为什么要区分APP Context 和Request Context
2.为什么要用栈结构来实现Contex
这篇文章主要参考知乎大佬Manjusaka这俩问题的情况,请移步那里吧.
简单来说就是:Werkzeug 内置的 Middleware 将两个 Flask App 组合成一个一个 WSGI Application。这种情况下两个 App 都同时在运行,只是根据 URL 的不同而将请求分发到不同的 App 上处理。
为什么不用Blueprint?

Blueprint 是在同一个 App 下运行。其挂在 App Context 上的相关信息都是一致的。但是如果要隔离彼此的信息的话,那么用 App Context 进行隔离,会比我们用变量名什么的隔离更为方便

Middleware 模式是 WSGI 中允许的特性,换句话来讲,我们将 Flask 和另外一个遵循 WSGI 协议的 web Framework (比如 Django)那么也是可行的。

g

数据库配置和其余的重要配置信息,就挂载 App 对象上。但是如果是一些用户代码,比如你不想一层层函数传数据的话,然后有一些变量需要传递,那么可以挂在 g 上。

下面来讲解一下下列代码的意思:

request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))
g = LocalProxy(partial(_lookup_app_object, 'g'))
def _lookup_app_object(name):
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return getattr(top, name)

partial偏函数,是将参数传递给偏函数的第一个参数(某函数对象),得到一个新的函数对象

比如

request = LocalProxy(partial(_lookup_req_object,'request'))

等价于
将'request'传递给_lookup_app_objec函数,生成一个无参数的函数对象,然后传递给LocalProxy类,进行实例化,执行LocalProxy的__init__方法
Proxy的代码如下:

class LocalProxy(object):

    __slots__ = ('__local', '__dict__', '__name__', '__wrapped__')
    #LocalProxy实例化,需要传递一个可调用对象(一般是函数如上面由偏函数生成的函数),然后可以给这个对象起一个名字name
    def __init__(self, local, name=None):
        # 注意下面一行代码是个坑,很大很大的坑,就是给self设定属性,_LocalProxy__local,等价于self.__local = ***
        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__)

 

    def __getattr__(self, name):
        # 当要获取代理的某个属性时,通过_get_current_object()拿到代理的对象,相当于__local属性加括号,执行那个可调用对象,拿到代理的东西
        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)
    __call__ = lambda x, *a, **kw: x._get_current_object()(*a, **kw)
    __getitem__ = lambda x, i: x._get_current_object()[i]
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容