欢迎光临!
若无相欠,怎会相见

源码阅读 – Flask v0.1 [05] Flask Context 上下文 <上>

序言

上篇文章写了 Flask 的路由系统,详情请看 源码阅读 – Flask v0.1 [04] Flask 路由系统 ,这篇文章写 Flask 的 Context 上下文 。

Context

Flask 提供了两种上下文 (Context) , 请求上下文和程序上下文 , 这两种上下文分别包含 requestsessioncurrent_appg 这四个变量 , 这些变量是实际对象的本地代理 (local proxy) , 因此被称为本地上下文 (context locals) 。 这些代理对象定义在 flask.py 脚本中 。

获取当前请求的信息是从 _request_ctx_stack.top 中获取出来的 , 也就是说请求会被加入请求栈中 , 栈顶就是当前请求 。 可以看一下这个请求栈 _request_ctx_stack 的定义 :

_request_ctx_stack = LocalStack()
current_app = LocalProxy(lambda: _request_ctx_stack.top.app)
request = LocalProxy(lambda: _request_ctx_stack.top.request)
session = LocalProxy(lambda: _request_ctx_stack.top.session)
g = LocalProxy(lambda: _request_ctx_stack.top.g)

我们在程序中从 flask 包直接导入的 request 和 session 就是定义在这里的全局对象 , 这两个对象是对实际的 request 变量和 session 变量的代理 。

通过请求栈 _request_ctx_stack 的定义可以看到 , 确实是一个请求栈 , 而且是一个多线程隔离的请求中 。 在这边我们简单理解 LocalStack 是一个多线程安全的栈 , 提供 push , pop , top 的方法 。 而栈中元素必然就是单个请求了 , 元素类型为 _RequestContext

[flask.py]

class _RequestContext(object):

    def __init__(self, app, environ):
        self.app = app
        self.url_adapter = app.url_map.bind_to_environ(environ)
        self.request = app.request_class(environ)
        self.session = app.open_session(self.request)
        self.g = _RequestGlobals()
        self.flashes = None

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

    def __exit__(self, exc_type, exc_value, tb):
        # do not pop the request stack if we are in debug mode and an
        # exception happened.  This will allow the debugger to still
        # access the request object in the interactive shell.
        if tb is None or not self.app.debug:
            _request_ctx_stack.pop()

看到单个请求使用 app 和 environ 进行初始化 , 其中 app 就是 Flask 实例 , environ 为单次请求的具体信息 。 其中就包含 url_adapter 属性 , 前面已经介绍过 , 就是通过 url_adapter.match() 进行匹配后获取到 endpointvalues 的 , 从而获取到请求处理的视图函数的 , 从而与前面的解释相互印证 。 那么现在还剩下一个问题 , flask 是什么时候将 _RequestContext 加入到 _request_ctx_stack 中的呢 ? 让我们回头看一下 wsgi_app() 方法 , 使用 with 进行调用 :

class Flask(object):

    def wsgi_app(self, environ, start_response):
        with self.request_context(environ):
            rv = self.preprocess_request()
            if rv is None:
                rv = self.dispatch_request()
            response = self.make_response(rv)
            response = self.process_response(response)
            return response(environ, start_response)

    def request_context(self, environ):
        return _RequestContext(self, environ)

可以看到调用了 request_context() 方法 , 此方法创建了一个 _RequestContext 对象 , 然后使用 with 的调用方式 , 会执行 _RequestContext__enter__() 魔术方法 , 即会发现 _request_ctx_stack.push(self) 执行 , 将创建的 _RequestContext 加入请求栈 _request_ctx_stack 中 , 然后在执行处理结束的时候 , 执行 __exit__() 方法 , 将请求从请求栈中移除 。 至此 , 一切豁然开朗 。

本地线程与 Local

如果每次只能发送一封电子邮件 (单线程) , 那么在发送大量邮件时会花费很多时间 , 这时就需要使用多线程技术 。 处理 HTTP 请求的服务器也是这样 , 当我们的程序需要面对大量用户同时发起的访问请求时 , 我们显然不能一个个地处理 。 这时就需要使用多线程技术 , Werkzeug 提供的开发服务器默认会开启多线程支持 。

在处理请求时使用多线程后 , 我们会面临一个问题 。 当我们直接导入 request 对象并在视图函数中使用时 , 如何确保这时的 request 对象包含的请求信息就是我们需要的那一个 ? 比如 A 用户和 B 用户在同一时间访问 hello 视图 , 这时服务器分配了两个线程来处理这两个请求 , 如何确保每个线程内的 request 对象都是各自对应 、 互不干扰的 ?

解决办法就是引入本地线程 (Thread Local) 的概念 , 在保存数据的同时记录下对应的线程 ID , 获取数据时根据所在线程的 ID 即可获取到对应的数据 。 就像是超市里的存包柜 , 每个柜子都有一个号码 , 每个号码对应一份物品 。

Flask 中的本地线程使用 Werkzeug 提供的 Local 类实现 , 如代码清单 :

[wekzeug/local.py]

try:
    from greenlet import getcurrent as get_current_greenlet
except ImportError: # pragma: no cover
    try:
        from py.magic import greenlet
        get_current_greenlet = greenlet.getcurrent
        del greenlet
    except:
        # catch all, py.* fails with so many different errors.
        get_current_greenlet = int

class Local(object):
    __slots__ = ('__storage__', '__lock__')

    def __init__(self):
        object.__setattr__(self, '__storage__', {})
        object.__setattr__(self, '__lock__', allocate_lock())

    def __iter__(self):
        return self.__storage__.iteritems()

    def __call__(self, proxy):
        """Create a proxy for a name."""
        return LocalProxy(self, proxy)

    def __release_local__(self):
        self.__storage__.pop(get_ident(), None)

    def __getattr__(self, name):
        self.__lock__.acquire()
        try:
            try:
                return self.__storage__[get_ident()][name]
            except KeyError:
                raise AttributeError(name)
        finally:
            self.__lock__.release()

    def __setattr__(self, name, value):
        self.__lock__.acquire()
        try:
            ident = get_ident()
            storage = self.__storage__
            if ident in storage:
                storage[ident][name] = value
            else:
                storage[ident] = {name: value}
        finally:
            self.__lock__.release()

    def __delattr__(self, name):
        self.__lock__.acquire()
        try:
            try:
                del self.__storage__[get_ident()][name]
            except KeyError:
                raise AttributeError(name)
        finally:
            self.__lock__.release()

Local 中构造函数定义了两个属性 , 分别是 __storage__ 属性和 __ident_func__ 属性 。 __storage__ 是一个嵌套的字典 , 外层的字典使用线程 ID 作为键来匹配内部的字典 , 内部的字典的值即真实对象 。 它使用 self.__storage__[self.__ident_func__()][name] 来获取数据 , 一个典型的 Local 实例中的 __storage__ 属性可能会是这样 :

{ 线程ID: { 名称: 实际数据}}

在存储数据时也会存入对应的线程 ID 。 这里的线程 ID 使用 __ident_func__ 属性定义的 get_ident() 方法获取 。 这就是为什么全局使用的上下文对象不会在多个线程中产生混乱 。

这里会优先使用 Greenlet 提供的协程 ID , 如果 Greenlet 不可用再使用 thread 模块获取线程 ID 。 类中定义了一些魔法方法来改变默认行为 。 比如 , 当类实例被调用时会创建一个 LocalProxy 对象 , 我们在后面会详细了解 。 除此之外 , 类中还定义了用来释放线程/协程的 __release_local__() 方法 , 它会清空当前线程/协程的数据 。

在 Python 类中 , 前后双下划线的方法常被称为魔法方法 (Magic Methods) 。 它们是 Python内置的特殊方法 , 我们可以通过重写这些方法来改变类的行为 。 比如 , 我们熟悉的 __init__() 方法 (构造函数) 会在类被实例化时调用 , 类中的 __repr__() 方法会在类实例被打印时调用 。 Local 类中定义的 __getattr__()__setattr__()__delattr__() 方法分别会在类属性被访问 、 设置 、 删除时调用 ; __iter__() 会在类实例被迭代时调用 ; __call__() 会在类实例被调用时调用 。 完整的列表可以在 Python 文档 (https://docs.python.org/3/reference/datamodel.html) 看到 。

堆栈与 LocalStack

堆栈或栈是一种常见的数据结构 , 它的主要特点就是后进先出 (LIFO,Last In First Out) , 指针在栈顶 (top) 位置 , 如图 16-9 所示 。 堆栈涉及的主要操作有 push (推入) 、 pop (取出) 和 peek (获取栈顶条目) 。 其他附加的操作还有获取条目数量 , 判断堆栈是否为空等 。 使用 Python 列表 (list) 实现的一个典型的堆栈结构如代码清单所示 。

[stack.py]

class Stack:

    def __init__(self):
        self.items = []

    def push(self, item): # 推入条目
        self.items.append(item)

    def pop(self): # 移除并返回栈顶条目
        if self.is_empty:
            return None
        return self.items.pop()

    @property
    def is_empty(self): # 判断是否为空
        return self.items == []

    @property
    def top(self): # 获取栈顶条目
        if self.is_empty:
            return None
        return self.items[-1]

承接上文 , 其中 push() 方法和 pop() 方法分别用于向堆栈中推入和删除一个条目 。 具体的操作示例如下 :

>>> class Stack:

        def __init__(self):
            self.items = []

        def push(self, item): # 推入条目
            self.items.append(item)

        def pop(self): # 移除并返回栈顶条目
            if self.is_empty:
                return None
            return self.items.pop()

        @property
        def is_empty(self): # 判断是否为空
            return self.items == []

        @property
        def top(self): # 获取栈顶条目
            if self.is_empty:
                return None
            return self.items[-1]


>>> s = Stack()
>>> s.push(42)
>>> s.top
42
>>> s.push(24)
>>> s.top
24
>>> s.pop()
24
>>> s.top
42
>>>

Flask 中的上下文对象正是存储在这一类型的栈结构中 , flask 这行代码创建了请求上下文堆栈 。

# context locals
_request_ctx_stack = LocalStack()

从这里可以想到 , 我们平时导入的 request 对象是保存在堆栈里的一个 _RequestContext 实例 , 导入的操作相当于获取堆栈的栈顶 (top) , 它会返回栈顶的对象 (peek操作) , 但并不删除它 。

这个堆栈对象使用 Werkzeug 提供的 LocalStack 类创建 , 如代码清单所示 :

class LocalStack(object):

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

    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)

    def push(self, obj):
        """Pushes a new item to the stack"""
        self._lock.acquire()
        try:
            rv = getattr(self._local, 'stack', None)
            if rv is None:
                self._local.stack = rv = []
            rv.append(obj)
            return rv
        finally:
            self._lock.release()

    def pop(self):
        """Removes the topmost item from the stack, will return the
        old value or `None` if the stack was already empty.
        """
        self._lock.acquire()
        try:
            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()
        finally:
            self._lock.release()

    @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

简单来说 , LocalStack 是基于 Local 实现的栈结构 (本地堆栈 , 即实现了本地线程的堆栈) , 和我们在前面编写的栈结构一样 , 有 push() 、 pop() 方法以及获取栈顶的 top 属性 。 在构造函数中创建了 Local() 类的实例 _local 。 它把数据存储到 Local 中 , 并将数据的字典名称设为 ‘stack’ 。 注意这里和 Local 类一样也定义了 __call__ 方法 , 当 LocalStack 实例被直接调用时 , 会返回栈顶对象的代理 , 即 LocalProxy 类实例 。

这时会产生一个疑问 , 为什么 Flask 使用 LocalStack 而不是直接使用 Local 存储上下文对象 。 主要的原因是为了支持多程序共存 。 将程序分离成多个程序很类似蓝本的模块化分离 , 但它们并不是一回事 。 前面我们提到过 , 使用 Werkzeug 提供的 DispatcherMiddleware 中间件就可以把多个程序组合成一个 WSGI 程序运行 。

在上面的例子中 , Werkzeug 会根据请求的 URL 来分发给对应的程序处理 。 在这种情况下 , 就会有多个上下文对象存在 , 使用栈结构就可以让多个程序上下文存在 ; 而活动的当前上下文总是可以在栈顶获得 , 所以我们从 _request_ctx_stack.top 属性来获取当前的请求上下文对象 。

结语

本篇文章先到这里,上下文被分成两个部分。

如有错误,敬请指出,感谢指正!       — 2021-06-30  22:33:43

赞(0) 打赏
转载请注明:飘零博客 » 源码阅读 – Flask v0.1 [05] Flask Context 上下文 <上>
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

欢迎光临