LocalStack和Local对象实现栈的管理

Flask的上下文

flask 中有两种上下文:application context 和 request context。上下文有关的内容定义在 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
 
 
# 请求上下文栈对象
_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 和 LocalProxy。它们两个的结果就是我们可以动态地获取两个上下文的内容,在并发程序中每个视图函数都会看到属于自己的上下文,而不会出现混乱。

LocalStack 和 LocalProxy 都是 werkzeug 提供的,定义在 local.py 文件中,分析之前,先了解一个类似于threading.local 的效果的类Local,它实现了多线程或者多协程情况下全局变量的隔离效果

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(object):
    __slots__ = ('__storage__', '__ident_func__')
 
    def __init__(self):
        # 数据保存在 __storage__ 中,产生了{'__storage__':{}}结构, 后续访问都是对该属性的操作
        object.__setattr__(self, '__storage__', {})
        object.__setattr__(self, '__ident_func__', get_ident)
 
    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)
 
    # 下面三个方法实现了属性的访问、设置和删除。
    # 注意到,内部都调用 `self.__ident_func__` 获取当前线程或者协程的 id,然后再访问对应的内部字典。
    # 如果访问或者删除的属性不存在,会抛出 AttributeError。
    # 这样,外部用户看到的就是它在访问实例的属性,完全不知道字典或者多线程/协程切换的实现
    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类的作用就是产生下面的数据结构
	{'__storage__':{
		'线程或者协程的 id':{name:value}
	}}  
`

了解了Local类后,回来分析LocalStack 和 LocalProxy

LocalStack 是基于 Local 实现的栈结构。如果说 Local 提供了多线程或者多协程隔离的属性访问,那么 LocalStack 就提供了隔离的栈访问

class LocalStack(object):
 
    def __init__(self):
        # 实例化Local类,并封装到LocalStack类中
        self._local = Local()
 
    def __release_local__(self):
        """可以用来清空当前线程或者协程的栈数据"""
        self._local.__release_local__()
 
    def __call__(self):
        """用于返回当前线程或者协程栈顶元素的代理对象。"""
        def _lookup():
            rv = self.top
            if rv is None:
                raise RuntimeError('object unbound')
            return rv
        return LocalProxy(_lookup)
 
    # push、pop 和 top 三个方法实现了栈的操作,
    # 可以看到栈的数据是保存在 self._local.stack 属性中的
    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对象:

# 实例化 请求上下文栈对象
_request_ctx_stack = LocalStack()

# 它会把当前线程或者协程的请求都保存在栈里,等使用的时候再从里面读取

LocalProxy 是一个 Local 对象的代理,负责把所有对自己的操作转发给内部的 Local 对象。

class LocalProxy(object):
    
    __slots__ = ('__local', '__dict__', '__name__')
 
    def __init__(self, local, name=None):
        # 产生了{'__local':local对象}结构,为什么_LocalProxy__local会变成__local,请参考面向对象的私有属性
        object.__setattr__(self, '_LocalProxy__local', local)
        object.__setattr__(self, '__name__', name)
 
    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__)
 
    @property
    def __dict__(self):
        try:
            return self._get_current_object().__dict__
        except RuntimeError:
            raise AttributeError('__dict__')
 
    def __getattr__(self, name):
        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

初步了解了LocalStack和LocalProxy类后,还要了解一个小知识点:

偏函数:
import functools
# 偏函数
def func(a1,a2):
    print(a1,a2)
# 帮func函数传递参数,并产生新的函数名
new_func = functools.partial(func,123)
# 新的函数只需要传一个参数
new_func(2)

现在再看下面的代码

_request_ctx_stack = LocalStack()
# 调用 _lookup_req_object方法,拿到封装在 请求上下文 里的request对象
request = LocalProxy(partial(_lookup_req_object, 'request'))
# 调用 _lookup_req_object方法,拿到封装在 请求上下文 里的session对象
session = LocalProxy(partial(_lookup_req_object, 'session'))

如果你在疑惑 :什么时候请求上下文里封装了request对象和session对象?那你可记得之前的wsgi_app方法:

def wsgi_app(self, environ, start_response):
    # 先创建RequestContext对象,并封装了request对象
    ctx = self.request_context(environ)
    error = None
    try:
        try:
            # ctx.push的内部完成了什么?
            # 1. 先建立了_request_ctx_stack对象(LocalStack),暂时内部数据结构{"__storage__":{}}}
            	# 完成功能代码: RequestContext/push方法里的 top = _request_ctx_stack.top
            # 2. 再创建_app_ctx_stack对象(LocalStack),暂时内部数据结构{"__storage__":{}}}
            	# 完成功能代码: RequestContext/push方法里的 app_ctx = _app_ctx_stack.top
            # 3. 再创建AppContext对象并封装了app和g
            	# 完成功能代码: RequestContext/push方法里的 app_ctx = self.app.app_context()
            # 4. 把AppContext对象压入到栈中,从而形成了{"__storage__":{线程ID:{'stack':[app_ctx对象]}}}数据结构
            	# 完成功能代码: app_ctx.push()
            # 5. 把RequestContext对象压入到栈中,从而形成了{"__storage__":{线程ID:{'stack':[ctx对象]}}}数据结构
            	# 完成功能代码: _request_ctx_stack.push(self)
            # 6. RequestContext对象封装session
            	# 完成功能代码: self.session = session_interface.open_session(self.app, self.request)
            ctx.push()
            response = self.full_dispatch_request()
        except Exception as e:
            error = e
            response = self.handle_exception(e)
        except:  
            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.push()的详细分析:

# RequestContext类中push方法
    def push(self):
        #1. 先建立了_request_ctx_stack对象(LocalStack),暂时内部数据结构{"__storage__":{}}}
        top = _request_ctx_stack.top
        # 2. 再创建_app_ctx_stack对象(LocalStack),暂时内部数据结构{"__storage__":{}}}
        app_ctx = _app_ctx_stack.top
        if app_ctx is None or app_ctx.app != self.app:
            # 3. 再创建AppContext对象并封装了app和g
            app_ctx = self.app.app_context()
            # 4. 把AppContext对象压入到栈中,从而形成了{"__storage__":{线程ID:{'stack':[app_ctx对象]}}}数据结构
            app_ctx.push()
            self._implicit_app_ctx_stack.append(app_ctx)
        else:
            self._implicit_app_ctx_stack.append(None)
		# 5. 把RequestContext对象压入到栈中,从而形成了{"__storage__":{线程ID:{'stack':[ctx对象]}}}数据结构
        _request_ctx_stack.push(self)

        if self.session is None:
            session_interface = self.app.session_interface
            # 6. RequestContext对象封装session
            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()

首先从下面两行代码开始:

ctx = self.request_context(environ)
ctx.push()

每次在调用 app.__call__ 的时候,都会把对应的请求信息压栈,最后执行完请求的处理之后把它出栈。

我们来看看request_context, 这个 方法只return一个类:

def request_context(self, environ):
    # 调用了 RequestContext,并把 self 和请求信息的字典 environ 当做参数传递进去
    return RequestContext(self, environ)
class RequestContext(object):

    def __init__(self, app, environ, request=None):
        self.app = app
        if request is None:
            request = app.request_class(environ)
        self.request = request
        self.url_adapter = app.create_url_adapter(self.request)
        self.match_request()

    def match_request(self):
        
        try:
            url_rule, self.request.view_args = 
                self.url_adapter.match(return_rule=True)
            self.request.url_rule = url_rule
        except HTTPException as e:
            self.request.routing_exception = e

    def push(self):
        """把该请求的 请求上下文和应用上下文 有关的信息保存到各自对应的栈上,具体看上面"""
        top = _request_ctx_stack.top
        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)

        _request_ctx_stack.push(self)

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

    def pop(self, exc=_sentinel):
        """ 和push相反,把 请求上下文和应用上下文 有关的信息从各自对应的栈上删除"""
        app_ctx = self._implicit_app_ctx_stack.pop()
        try:
            clear_request = False
            if not self._implicit_app_ctx_stack:
                self.app.do_teardown_request(exc)

                request_close = getattr(self.request, 'close', None)
                if request_close is not None:
                    request_close()
                clear_request = True
        finally:
            rv = _request_ctx_stack.pop()
            if clear_request:
                rv.request.environ['werkzeug.request'] = None
            if app_ctx is not None:
                app_ctx.pop(exc)

    def auto_pop(self, exc):
        if self.request.environ.get('flask._preserve_context') or 
           (exc is not None and self.app.preserve_context_on_exception):
            self.preserved = True
            self._preserved_exc = exc
        else:
            self.pop(exc)

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

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

每个 request context 都保存了当前请求的信息,比如 request 对象和 app 对象,初始化的最后调用了 match_request 实现了路由的匹配逻辑

push 操作就是把该请求的 ApplicationContext(如果 _app_ctx_stack 栈顶不是当前请求所在 app ,需要创建新的 app context) 和 RequestContext 有关的信息保存到对应的栈上,压栈后还会保存 session 的信息; pop 则相反,把 request context 和 application context 出栈,做一些清理性的工作。

结论:

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

上面分析过后,你可能有三个疑惑:

# 1. 不会产生多个app吗?
	`不会,此处app对象运用了单例模式,所以只有一个app对象,因此多个 request 共享了 application context`
# 2. 为什么要把 请求上下文 和 应用上下文 分开?每个请求不是都同时拥有这两个上下文信息吗?
	`因为'灵活性',虽然在实际运行中,每个请求对应一个 请求上下文 和一个 应用上下文,但是在测试或者 python shell 中运行的时候,用户可以单独创建 请求上下文 或者 应用上下文,这种灵活度方便用户的不同的使用场景;`
# 3. 为什么 请求上下文 和 应用上下文 都有实现成栈的结构?每个请求难道会出现多个 请求上下文 或者 应用上下文 吗?
	`在web runtime 时,栈永远只有1个对象。但是在写离线脚本时,才会用在栈中放多个对象.(创建一个py文件本地运行)`

第三个问题的代码示例:

# --------------------------例1------------------------------------
from flask import current_app,g
from pro_excel import create_app

app1 = create_app()
with app1.app_context():  # AppContext对象(app,g) -> local对象
    print(current_app.config) # -1 top app1 
    app2 = create_app()
    with app2.app_context():  # AppContext对象(app,g) -> local对象
        print(current_app.config) # top -1 app2 
    print(current_app.config) # top -1 app1

# 写离线脚本且多个上下文嵌套时,才会在栈中添加多个对象。


# ---------------------------例2-----------------------------------
from werkzeug.wsgi import DispatcherMiddleware
from frontend_app import application as frontend
from backend_app import application as backend
 
application = DispatcherMiddleware(frontend, {
    '/backend':     backend
})
# 使用 werkzeug 的 DispatcherMiddleware 实现多个 app 的分发,这种情况下 _app_ctx_stack 栈里会出现两个 application context。
posted @ 2019-12-01 19:21  影印  阅读(285)  评论(0编辑  收藏  举报
……