序言
上篇文章写了 Flask 的路由系统,详情请看 源码阅读 – Flask v0.1 [04] Flask 路由系统 ,这篇文章写 Flask 的 Context 上下文 。
Context
Flask 提供了两种上下文 (Context) , 请求上下文和程序上下文 , 这两种上下文分别包含 request
、 session
和 current_app
、 g
这四个变量 , 这些变量是实际对象的本地代理 (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()
进行匹配后获取到 endpoint
和 values
的 , 从而获取到请求处理的视图函数的 , 从而与前面的解释相互印证 。 那么现在还剩下一个问题 , 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
最新评论
这个软件有bug的,客户端windows有些键不能用如逗号、句号
没有收到邮件通知
我的评论通知貌似坏掉了,定位一下问题
测试一下重新部署后的邮件功能
居然看到自己公司的MIB库,诚惶诚恐
那可能是RobotFramework-ride的版本问题。我装的1.7.4.2,有这个限制。我有空再尝试下旧版本吧,感谢回复。
你好!我在python2.7中安装RobotFramework-ride的时候提示wxPython的版本最高是2.18.12,用pip下载的wxPython版本是4.10,而且我在那个路径下没有找到2
真的太好了,太感谢了,在bilibili和CSDN上都找遍了,终于在你这里找到了