Flask进击篇(2)——Flask上下文管理

本文首发于微信公众号:战渣渣
欢迎大家关注。

关联知识

WEB开发——Python WSGI协议详解
Flask进击篇(1)——Flask运行流程

背景

在Flask中可直接导入from flask import request, current_app, g并直接使用,那Flask是如何保证这个request对象就是这个请求对象呢?

Flask官方中有提到使用的是本地线程对象,这篇文章就来揭示其原理

Flask线程间上下文安全

Falsk完成线程安全的原理,是在启动之后进程里维护request栈和app栈,栈是通过线程ID来保证每个请求的线程安全。

实现主要依赖三个类Local,LocalStack和LocalProxy,下面看一下具体的实现原理

三个类构建本地数据

1. Local

先看Local的源码,实质并不是Flask中定义的,而是Flask依赖的werkzeug库所定义。

# werkzeug\local.py

# 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

# 实际的Local类
class Local(object):
    __slots__ = ('__storage__', '__ident_func__')

    def __init__(self):
        object.__setattr__(self, '__storage__', {})
        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}

可以看到其定义的两个属性__storage__, __ident_func__以及三个方法__getattr__,__setattr__,__release_local__。

  1. 属性__storage__是多层级字典,第一层key是隐含的线程ID或者协程ID,第二层的key是实际使用的关键字

  2. 属性__ident_func__可以看到是get_ident函数,get_ident函数要么是通过thread库获取当前执行单元的线程ID,要么是通过greenlet库获取当前执行协程的协程ID。

另外可以看到Local这个类的三个方法,实质是通过重写Python内置函数__setattr__和__getattr__来实现线程或者协程间数据隔离

  1. 获取local某属性时

    如:local.age实质触发的是__getattr__方法

    1. 先获取到当前线程ID——__ident_func__函数获取,然后在__storage__字典中找到线程ident对应的结果集

    2. 从获取到的结果中再查找age属性

  2. 设置local某属性时

    如:local.age = 12 实际触发的是__setattr__方法

    先获取到当前线程ID——__ident_func__函数获取,然后在__storage__字典设置相应的属性字典集

另一个__release_local__方法就是将相应的线程数据删除。

画个简图比较起来更直观一些。

Local处理流程

主线程中生成一个对象local=Local(),三个线程中进行相同的操作local.no=每个线程对应的数。为每个线程都开辟一个存储,所以谁来取或者存就找到自己对应中的位置,虽然取得key都一样,但是每次存取都是只关于自己的值。

2. LocalStack

LocalStack也是定义在Flask所依赖的werkzeug库,从字面意思来理解,它就是Local的堆栈操作,看一下源码如何定义。

class LocalStack(object):
    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):
        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):
        try:
            return self._local.stack[-1]
        except (AttributeError, IndexError):
            return None

LocalStack实质就是围绕着Local来进行操作,根据上面我们读完Local的源码可以看到,

  1. LocalStack定义了一个Local对象
  2. 给这个对象设置了一个stack属性,且这个属性是一个列表
  3. LocalStack中定义了对这个列表进行压栈,出栈等方法
  4. 给类中的Local对象提供了自定义ident_func的方法

3. LocalProxy

LocalProxy字面意思就是做一个Local的代理,我们先从一个request的定义来看LocalProxy的用法,然后结合源码来看LocalProxy到底是用来做什么?

# venv/Lib/site-packages/werkzeug/local.py

@implements_bool
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):
        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):
        if name == "__members__":
            return dir(self._get_current_object())
        return getattr(self._get_current_object(), name)


    __setattr__ = lambda x, n, v: setattr(x._get_current_object(), n, v)

类中稍微有些难理解的就是关于object.__setattr__(self, "_LocalProxy__local", local)的作用,实际就是给self设置一个__local属性。这是Python类中关于私有变量的定义。可以看Python的官方定义python私有变量

可以看到这个类将所有Python类所内置的方法都进行重写,重写后所有的操作都是基于类中所定义的_get_current_object方法返回的对象进行操作。

而这个方法中返回值就是初始化时所给定的local对象执行返回的结果。如果创建时指定的不是Local对象,则直接执行此方法。如果给定的是Local对象,则根据类名查找对应的对象。

现在这个比较抽象,这个代理到底是做的什么? 我们结合Flask定义全局的request对象来看。假如我们想获取请求的方法是什么,那我们使用的就是request.method。

下面是request定义的源码

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

request = LocalProxy(partial(_lookup_req_object, "request"))
  1. 根据LocalProxy的源码中重写的__getattr__方法,先执行_get_current_object方法获取到对象,然后再获取返回对象method属性。​
  2. 创建LocalProxy时传递的函数是_lookup_req_object的偏函数,实际就是_lookup_req_object且name=request
  3. 再LocalProxy中__local就是一个函数,所以在执行_get_current_object就是执行_lookup_req_object且name=request返回的值,然后再取其method属性
  4. 此时再执行_lookup_req_object函数,从_request_ctx_stack获取top的request

使用Proxy可以简单快捷的使用request.method获取相应的值,其核心就是每次获取时都会执行对应的函数,而函数中每次返回的值都是线程安全。保证数据正确且优雅。 否则我们每次都去执行一个函数来获取其值,然后再取其属性。

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

推荐阅读更多精彩内容