Flask上下文管理

认识Flask的上下文流程,需要知道threading.local()的原理

线程数据安全的三种情况:

情况一: 单进程单线程, 基于全局变量
情况二: 单进程多线程, threading.local实现线程数据隔离
情况三: 单进程单线程(携程), threading.local不能实现数据隔离, 但基于threading.local的思想,通过获取携程的唯一标识, 创建字典的形式存储数据

数据形式如: 
{  
    1368 : { k : v }
    1339 : { k : v }
    ...
}

模仿threading.local的思想,手动实现获取线程唯一标识存储数据:

import threading
from _thread import get_ident

class Local():
    def __init__(self):
        # 定义一个空字典, 用于存储不同线程下的数据
        self.storage = { }
        # 将get_ident方法赋值到Local对象中,后续获取线程唯一标识使用
        self.get_ident = get_ident

    # 设置数据
    def set(self, k, v):
        ident = self.get_ident()  # 获取线程标识
        origin = self.storage.get(ident)  # 获取线程的数据字典
        if not origin:  # 若origin为空,则说明该线程还没有数据
            origin = {k:v}  # 生成该线程的数据字典
        else:
            origin[k] = v  # 若origin不为空,则说明该线程以及有了数据字典,只需要把新的数据添加到字典中
        self.storage[ident] = origin  # 将线程标识和数据字典添加到storage这个大字典中

    # 获取数据
    def get(self, k):
        ident = self.get_ident()  # 获取线程标识
        origin = self.storage.get(ident)  # 获取线程的数据字典
        if not origin:
            return None
        else:
            v = origin.get(k, None)
        return v

local_value = Local()

def task(num):
    local_value.set('name', num)  # 为各自的线程空间赋值
    import time
    time.sleep(1)
    print(local_value.get('name'), threading.current_thread().name)

for i in range(5):
    th = threading.Thread(target=task, args=(i,), name='线程%s' % i)
    th.start()

基于上面的思想,把获取线程唯一标识换成获取携程的唯一标识,为每个携程存储数据

如何获取携程唯一标识?

pip install gevent  # 安装gevent模块时自动安装greenlet模块

from greenlet import getcurrent as get_ident  # 导入getcurrent方法,该方法可以获取携程唯一标识

手动实现获取携程程唯一标识存储数据:

与上面的获取线程唯一标识思路一样

import threading

# 支持携程优先使用携程,否则使用线程
try:
    from greenlet import getcurrent as get_ident
except ImportError:
    try:
        from thread import get_ident
    except ImportError:
        from _thread import get_ident

class Local():
    def __init__(self):
        # 定义一个空字典, 用于存储不同线程下的数据
        self.storage = { }
        # 将get_ident方法赋值到Local对象中,后续获取线程唯一标识使用
        self.get_ident = get_ident

    # 设置数据
    def set(self, k, v):
        ident = self.get_ident()  # 获取线程标识
        origin = self.storage.get(ident)  # 获取线程的数据字典
        if not origin:  # 若origin为空,则说明该线程还没有数据
            origin = {k:v}  # 生成该线程的数据字典
        else:
            origin[k] = v  # 若origin不为空,则说明该线程以及有了数据字典,只需要把新的数据添加到字典中
        self.storage[ident] = origin  # 将线程标识和数据字典添加到storage这个大字典中

    # 获取数据
    def get(self, k):
        ident = self.get_ident()  # 获取线程标识
        origin = self.storage.get(ident)  # 获取线程的数据字典
        if not origin:
            return None
        else:
            v = origin.get(k, None)
        return v

local_value = Local()

def task(num):
    local_value.set('name', num)  # 为各自的线程空间赋值
    import time
    time.sleep(1)
    print(local_value.get('name'), threading.current_thread().name)

for i in range(5):
    th = threading.Thread(target=task, args=(i,), name='线程%s' % i)
    th.start()

Flask上下文源码 之 LocalStack()

补充知识:

构造函数内有两种为对象赋值的方法
def __init__(self)
    self.name = { }
    object.__setattr__(self, 'name', { })  # 该方法通常在类中定义__setattr__方法时使用(防止递归)

LocalStack() 中实例化 Local(), Local()与 threading.local的整体思路一致

_request_ctx_stack = LocalStack()  # 点进去查看到下面的代码

try:
    # 判断是否支持携程, 若支持则优先使用携程,并导入getcurrent, 用于获取携程唯一标识
    from greenlet import getcurrent as get_ident
except ImportError:
    try:
        # 若不支持携程,则使用线程, 并导入thread, 用于获取获取线程唯一标识
        from thread import get_ident
    except ImportError:
        from _thread import get_ident

说明: 
python2中是 thread
python3中是 _thread

class LocalStack(object):
    def __init__(self):
        self._local = Local()  # 实例化Local()
    ......


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

    def __init__(self):
        object.__setattr__(self, "__storage__", {})  # self.__storage__ = { }
        object.__setattr__(self, "__ident_func__", get_ident)  # 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  # 将各自线程的数据存到各自的数据字典中 {ident:{'key':'value'}}
        except KeyError:
            storage[ident] = {name: value}  # 若该线程之前不存在,则创建该线程的数据字典

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

上下文流程:

class Flask()
    ...
    1.
    def run(self, host=None, port=None, debug=None, load_dotenv=True, **options):
        ...
        from werkzeug.serving import run_simple
        try:
            2.
            run_simple(host, port, self, **options) # 这里的 self 是 Flask 对象, 这里的self会加括号执行Flask()里的__call__方法
        finally:
            ...
    4.
    def wsgi_app(self, environ, start_response):
        # 将请求相关的数据environ封装到了request_context中的RequestContext类中
        ctx = self.request_context(environ)  # ctx是RequestContext的对象, ctx包含了所有请求相关的数据
        ...
    5.
    def request_context(self, environ):
        return RequestContext(self, environ)  # self: 是Flask对象app
    3.
    def __call__(self, environ, start_response):
         return self.wsgi_app(environ, start_response)  # environ: 是基于HTTP请求的所有数据(请求首行,头,体)

1. app.run 触发 Flask()中的run方法
2. run方法中核心是导入 from werkzeug.serving import run_simple 触发
run_simple(host, port, self, **options) 注意,这里的 self 是 Flask 对象, 这里的self会加括号执行Flask()里的__call__方法
3.Flask()里的__call__才是入口, 这里调用了wsgi_app方法
4.Flask()里的wsgi_app方法封装了请求的所有信息,封装到了RequestContext类中

请求上下文:

通过上面的上下文流程,能找到请求相关的数据别封装到了RequestContext中

1.请求来 将请求数据添加到对应的线程数据字典中

2.请求走 将请求数据从对应的线程数据字典中删除

class Flask():
    ...
    1.
    def wsgi_app(self, environ, start_response):
        # 将请求相关的数据environ封装到了request_context中
        2.
        ctx = self.request_context(environ)  # ctx是RequestContext的对象
        error = None
        try:
            try:
                3.
                ctx.push()  # ctx调用push方法, push是对象绑定方法, 默认将ctx传入
            ...
        finally:
            ...
            7.
            ctx.auto_pop(error)

class RequestContext(object):
    ...
    4.
    def push(self):
        ...
         5.
         _request_ctx_stack.push(self)  # self是ctx  _request_ctx_stack是LocalStack()的对象
        # 将请求相关的数据push到_request_ctx_stack中

class LocalStack(object):
    def __init__(self):
        self._local = Local()  # Local()核心是为每个线程或者携程创建自己的数据存储空间
    6. 将请求信息放到各自线程的数据字典中
    def push(self, obj):  # 这里的obj是ctx对象, 请求相关的所有数据
        rv = getattr(self._local, "stack", None)  # self._local是 Local()对象  补充:{唯一标识: {'stack':[ctx,]}} 
        if rv is None:
            self._local.stack = rv = []  # 这里触发Local()中的__setattr__方法
        rv.append(obj)  # 将ctx放到Local()对象中
        return rv
    8. 将请求信息从各自线程的数据字典中提出
    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()

问: 谈谈Flask上下文管理

答: 

1.首先解释threading.local的原理, Flask为是支持携程,基于threading.local的思想,自己实现Loacl类,在Local类中通过获取携程唯一标识,存储携程自己的数据信息

2.请求刚进来触发run_simple中的Flask对象,进而触发Flask对象的__call__方法,调用wsgi_app方法

    2.1 首先封装了请求的所有信息到RequestContext中,这里封装了两层,一层是RequestContext, 另一层是request_class(注意: RequestContext里还封装了session)

    2.2 RequestContext对象ctx.push,将请求相关的数据push到_request_ctx_stack中,即LocalStack()中

    2.3 LocalStack()中实例化了Loacl(), 将请求相关的数据存到各自的线程数据字典中

3. 当我们导入使用request时, 调用的是全局的request对象,这个request对象是LocalProxy实例化的,当使用request里的方法(如: request.form)时, 会调用LocalProxy中的_get_current_object方法去Loacl()中将RequestContext的对象拿出来(RequestContext封装了请求的数据),最终就是 ctx.request.form, 核心就是拿到RequestContext的对象,即ctx

4. 当请求走的时候,会执行ctx.auto_pop方法调用LocalStack()中的pop方法,将Loacl()中存储的RequestContext对象删除

应用上下文

问: 请求扩展里的 before_request 会在请求到达视图函数前执行, 那么,在before_request中设置一个值, 把这个值传给视图函数的办法

答: 可以将这个值赋值到 request 中, 或赋值到 session 中, 这种方法可以, 但是很有可能这个值会与request或session中的数据冲突,

所以,基于以上问题, Flask中提供了一个 g对象, g对象是一个全局对象, 可以将上述问题的值赋值到g对象中

介绍: g对象是一个容器,每个请求周期都会创建一个g,用于在请求周期中传递值时存值使用

注意: g对象赋的值,在一个周期内可使用

案例:

from flask import Flask, request, g

app = Flask(__name__)

@app.before_request
def before():
    # 设置值
    request.xxx = 123
    g.xxx = 123
    pass

@app.route('/', methods=['GET', ])
def index():
    # 获取值
    print(request.xxx)
    print(g.xxx)
    return 'index'

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

源码:

首先一步步找到wsgi_app方法

def wsgi_app(self, environ, start_response):
    ctx = self.request_context(environ)  # ctx是RequestContext的对象
        error = None
        try:
            try:
                1.
                ctx.push()  # 这是核心请求上下文和应用上下文都在这里开始
        finally:
            ...
            9. 
            ctx.auto_pop(error)


class RequestContext(object):
    2.
    def push(self):
        ...
        if app_ctx is None or app_ctx.app != self.app:
            # 应用上下文入口:
            # app_context里面实例化AppContext(), 创建了 g 对象, g相当是一个全局字典
            3.
            app_ctx = self.app.app_context()  # app_ctx就是AppContext()的对象
            4.
            app_ctx.push()  # 调用AppContext()中的push方法

class AppContext(object):
    def __init__(self, app):
        self.app = app
        self.url_adapter = app.create_url_adapter(None)
        5.  app_ctx中创建g对象
        self.g = app.app_ctx_globals_class()
    6.
    def push(self):
        self._refcnt += 1
        if hasattr(sys, "exc_clear"):
            sys.exc_clear()
        7. 调用LocalStack()中的push方法
        _app_ctx_stack.push(self)  # _app_ctx_stack是LocalStack()的对象
        appcontext_pushed.send(self.app)

class LocalStack(object):
    8.
    def push(self, obj):  # 这里的obj是ctx对象, 请求相关的所有数据
        """Pushes a new item to the stack"""
        rv = getattr(self._local, "stack", None)  # self._local是 Local()对象
        if rv is None:
            self._local.stack = rv = []  # 这里触发Local()中的__setattr__方法
        rv.append(obj)  # 将ctx放到Local()对象中
        return rv
    10.
    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()
'''
    请求上下文存储形式: 
    _request_ctx_stack,local = {
        '唯一标识': {'stack' : [ ctx, ]} 
    }
    应用上下文存储形式:
    _app_ctx_stack.local = {
        '唯一标识': {'stack' : [ app_ctx, ]} 
    }
    
    app_ctx = AppContext()
    app_ctx对象里有 app, g
'''

问: 谈谈Flask应用上下文

答:

1.请求刚进来触发run_simple中的Flask对象,进而触发Flask对象的__call__方法,调用wsgi_app方法

2.首先封装了请求的所有信息到RequestContext中,这里封装了两层,一层是RequestContext, 另一层是request_class(注意: RequestContext里还封装了session)

3.RequestContext对象ctx.push,触发AppContext()类的实例化, 实例化时通过 app.app_ctx_globals_class() 创建了g对象

4.app_ctx 即AppContext()类的对象调用push方法,触发了 _app_ctx_stack.push方法,即LocalStack()类中push方法, 将app_ctx存储到各自线程的数据字典中

5.当请求走的时候,会执行ctx.auto_pop方法调用LocalStack()中的pop方法,将Loacl()中存储的AppContext()的对象删除

补充

Flask源码中 globals.py文件

def _lookup_req_object(name):  # name 是 'request' 或者是 'session'
    top = _request_ctx_stack.top  # _app_ctx_stack.top的返回值是 ctx 对象
    if top is None:
        raise RuntimeError(_request_ctx_err_msg)
    return getattr(top, name)  # 从ctx中获取request 或者 获取session


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  # 等同于 ctx.app  app是Flask对象


# context locals
_request_ctx_stack = LocalStack()  # 主要功能 实例化Local(), 提供push方法,将请求信息放到各自线程的数据字典中. 提供pop方法, 将请求信息从各自线程的数据字典中提出
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)

# 注意: 这里的偏函数里的 _lookup_req_object 会找到 ctx 对象
# 请求上下文
request = LocalProxy(partial(_lookup_req_object, "request"))  # 目标: 去Local()中获取ctx, 在ctx中获取request
session = LocalProxy(partial(_lookup_req_object, "session"))  # 目标: 去Local()中获取ctx, 在ctx中获取session
# 应用上下文
g = LocalProxy(partial(_lookup_app_object, "g"))

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值