Flask 上下文

Flask 上下文

Flask 中定义了两种上下文,分别为请求上下文和应用上下文,其中前者包括了 current_app (当前的应用对象)、g (处理请求时用作临时存储的对象);后者包括了 request (请求对象,封装了 HTTP 请求的内容)、session (用于存储请求之间需要记住的值)。

Flask 分发请求之前会激活请求上下文,请求处理完成后再将其删除,为了支持多个 app,Flask 中的 Context 是通过栈来实现。

简介

实际上所谓的上下文,在该场景下就是包括了一次请求所包含的信息,包括了从客户(一般是浏览器)发送过来的数据,例如,登陆时使用的用户名密码;以及在中间处理过程中生成的数据,例如,每次请求时我们可能会需要新建一个数据库链接。

Flask 会在接收每次请求的时候将参数自动转换为相应的对象,也就是 request、session,一般来说上下文传递可以通过参数进行,这也就意味这每个需要该上下文的函数都要多增加一个入参,为了解决这一问题,Flask 提供了一个类似于全局变量的实现方式(如下会讲到这一参数是线程安全的)。

在多线程服务器中,通过线程池处理不同客户的不同请求,当收到请求后,会选一个线程进行处理,请求的临时对象(也就是上下文)会保存在该线程对应的全局变量中(通过线程 ID 区分),这样即不干扰其他线程,又使得所有线程都可以访问。

Flask 上下文对象

如上所述,Flask 有两种上下文,分别是:

RequestContext 请求上下文

  • Request 请求的对象,会封装每次的 Http 请求 (environ) 的内容;
  • Session 会根据请求中的 cookie,重新载入该访问者相关的会话信息。

AppContext 程序上下文

  • g 处理请求时用作临时存储的对象,每次请求都会重设这个变量;
  • current_app 当前激活程序的程序实例。

生命周期

  • current_app 的生命周期最长,只要当前程序实例还在运行,都不会失效。
  • request 和 g 的生命周期为一次请求期间,当请求处理完成后,生命周期也就完结了。
  • session 就是传统意义上的 session,只要还未失效(用户未关闭浏览器、没有超过设定的失效时间),那么不同的请求会共用同样的 session。

线程安全

首先我们看看 request 是如何实现的,实际上之所以有 request 就是为了在多线程(或者协程)的环境下,各个线程可以使用各自的变量,不至于会混乱,接下来我们看看具体是如何实现的。

简介

实际上,Python 提供了同样类似的线程安全变量保存机制,也就是 threading.local() 方法,而在 flask 中,使用的是 werkzeug 中的 Local 实现的,详细可以参考 werkzeug.pocoo.org/docs/local

总体来说,werkzeug 提供了与 threading.local() 相同的机制,不过是 threading 只提供了线程的安全,对于 greenlet 则无效。

flask 定义

首先,查看源码目录下的 init.py 文件,可以发现这些变量实际上是在 globals.py 中定义,然后只是在 init.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)

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

其中,partial() 函数就是函数调用时,如果有多个参数,但其中的一个参数已经知道了,我们可以通过这个函数重新绑定一个新函数,然后去调用这个新函数,如果有默认参数的话,也可以自动对应。详见 偏函数。

而上述的代码,实际就定义了最终调用的是 getattr(top, ‘request’) 、 getattr(top, ‘session’) 。

如上,我们会发现 g、request、session 的调用方式都是一样的,最后都通过 getattr(top, name) 获取,这也就意味着有一个上下文对象同时保持了上述的三个对象。

另外,使用时,我们只要一处导入 request,在任何视图函数中都可以使用 request,关键是每次的都是不同的 request 对象,说明获取 request 对象肯定是一个动态的操作,不然肯定都是相同的 request。

之所以可以做到这样,主要就是 _lookup_req_object() 和 LocalProxy 组合完成的。

LocalProxy

LocalProxy 是 werkzeug/local.py 中定义的一个代理对象,在此处的作用是将所有的请求都发给内部的 _local 对象,如下的 __slots__ 用来表示所有的实例都只有一个实现。

class LocalProxy(object):
    
    __slots__ = ('__local', '__dict__', '__name__')

    def __init__(self, local, name=None):
        # 在上述的request中传递的就是_lookup_req_object,如果 传递给 _lookup_req_object 的参数是'request',那么 _lookup_req_object() 就是 request 对象
        object.__setattr__(self, '_LocalProxy__local', local)
        object.__setattr__(self, '__name__', name)

    def _get_current_object(self):
        # _lookup_req_object 中没有 __release_local__,直接进入该分支
        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)

当调用 request.method 时会调用 __getattr__() 函数,而实际上 request 对任何方法的调用,最终都会转化为对 _lookup_req_object() 返回对象的调用。

上述的 request.method 会调用 _request_ctx_stack.top.request.method 。

LocalStack

既然每次 request 都不同,要么调用 top = _request_ctx_stack.top 返回的 top 不同,要么 top.request 属性不同,在 flask 中每次返回的 top 是不一样的,所以 request 的各个属性都是变化的。

现在需要看看 _request_ctx_stack = LocalStack() 了,LocalStack 其实就是简单的模拟了堆栈的基本操作,包括 push、top、pop,其内部保存了与线程相关联的本地变量,从而使其在多线程中 request 不混乱。

LocalStack 部分源码,实现了 push pop pop 方法, 对 local 的操作

class LocalStack(object):

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

    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


Local

Local 部分源码

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

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

    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)

__storage__ 为内部保存的数据,key 是 线程或协程的 id ,也就是根据线程的标示符返回对应的值。

__storage__ 的数据结构

__storage__ = {

	ident1 : {
		'stack' : [ctx1,ctx2,...]
	},
    ident2 : {
		'stack' : [ctx1,ctx2,...]
	},
	...
	
}

源码分析

分析上下文是如何实现的。

上下文对象定义

应用上下文和请求上下文在 ctx.py 中定义,内容如下。

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.flashes = None
        self.session = None

RequestContext 类是请求上下文,里边保存了 app(当前对象的引用)与常用的 request , session

class AppContext(object):

    def __init__(self, app):
        self.app = app
        self.url_adapter = app.create_url_adapter(None)
        self.g = app.app_ctx_globals_class()

AppContext 类即是应用上下文,里面保存了 app (当前应用对象的引用)、g (用来保存需要在每个请求中需要用到的请求内全局变量) 。

流程

入口

flask是遵循WSGI接口的web框架,因此它会实现一个类似如下形式的函数以供服务器调用:

def application(environ, start_response): #一个符合wsgi协议的应用程序写法应该接受2个参数  
    start_response('200 OK', [('Content-Type', 'text/html')])  #environ为http的相关信息,如请求头等 start_response则是响应信息  
    return [b'<h1>Hello, web!</h1>']        #return出来是响应内容

这个application在flask里叫做wsgi_app。服务器框架在接收到http请求的时候,去调用app时,他实际上是用了Flask 的 __call__方法。

# Flask 部分源码
class Flask(_PackageBoundObject):
    
    def __call__(self, environ, start_response):
        """The WSGI server calls the Flask application object as the
        WSGI application. This calls :meth:`wsgi_app` which can be
        wrapped to applying middleware."""
        return self.wsgi_app(environ, start_response)

在Flask类中又调用了 wsgi_app(environ, start_response) 方法

实际上,当http请求从server发送过来的时候,便会调用 wsgi_app 方法并传入environ和start_response。

wsgi_app

    def wsgi_app(self, environ, start_response):
        ctx = self.request_context(environ) # 创建请求上下文对象 RequestContext
        error = None
        try:
            try:
                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)

wsgi_app 中,创建 请求上下文对象 ,并将其入栈,下面是入栈操作

class RequestContext(object):
	def push(self):
        ...
        _request_ctx_stack.push(self)	# 请求上下文入栈

可以看到 ctx.push() 将 ctx 压入到 _request_ctx_stack 这个栈中,所以当我们调用 request.METHOD 时将调用 _lookup_req_object() 函数 拿到 top 。

top 此时就是上面压入的 ctx 上下文对象(RequestContext 类的实例),而 getattr(top, “request”) 将返回 ctx 实例中的 request 请求,而这个 request 就是在 ctx 的 __init__() 中根据环境变量创建的。

这下应该清楚了,每次请求在调用视图函数之前,flask 会自动把请求创建好 ctx 上下文,并存放在线程的栈中,当使用时就可以根据线程 id 拿到了所需要的变量。

总流程图

Flask请求上下文

为什么要使用栈

到此,实际上已经大概清除了 request 的工作流程了,但是通常对于多线程来说,一个线程一次只会处理一个请求,也就是说当前的 request 应该是只有一个,那么为什么不是直接使用这个对象,而是要使用栈呢?

这主要是多个应用导致的,这也是 Flask 设计时的标准之一吧,也就是在一个 Python 进程中,可以拥有多个应用。由于一般都只有一个 app,栈顶存放的肯定是当前 request 对象,但是如果是多个 app,那么栈顶存放的是当前活跃的 request,也就是说使用栈是为了获取当前的活跃 request 对象。

__storage__ 的数据结构

__storage__ = {

	ident1 : {
		'stack' : [ctx1,ctx2,...]
	},
    ident2 : {
		'stack' : [ctx1,ctx2,...]
	},
	...
	
}

为什么要使用 LocalProxy

可是说了这么多,为什么一定要用proxy,而不能直接调用Local或LocalStack对象呢?这主要是在有多个可供调用的对象的时候会出现问题,如下图:

1563846975356

我们再通过下面的代码也许可以看出一二:

# use Local object directly
from werkzeug.local import LocalStack
user_stack = LocalStack()
user_stack.push({'name': 'Bob'})
user_stack.push({'name': 'John'})

def get_user():
    # do something to get User object and return it
    return user_stack.pop()


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

打印结果是:

John
John

再看下使用LocalProxy

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

def get_user():
    # do something to get User object and return it
    return user_stack.pop()

# 通过LocalProxy使用user对象
user = LocalProxy(get_user)
print user['name']
print user['name']

打印结果是:

John
Bob

怎么样,看出区别了吧,直接使用LocalStack对象,user一旦赋值就无法再动态更新了,而使用Proxy,每次调用操作符(这里[]操作符用于获取属性),都会重新获取user,从而实现了动态更新user的效果。见下图:

1563847078000

proxy auto select object

Flask以及Flask的插件很多时候都需要这种动态更新的效果,因此LocalProxy就会非常有用了。

结论

在 Flask 中包括了两种上下文,应用上下文(app,g)和请求上下文(request,session)。使用起来,它们可能被认为是一些全局变量,所有请求的数据均在其中。

实际上,它们都处于一个请求的局部中,对于每个请求都是独立的,通过一个特殊的数据结构来动态的设置、获取它们。

参考

Flask 上下文理解

Flask进阶(一)——请求上下文和应用上下文完全解答(上)

Werkzeug(Flask)之Local、LocalStack和LocalProxy

posted @ 2019-08-10 00:37  写bug的日子  阅读(139)  评论(0编辑  收藏  举报