04. Flsak源码分析之【上下文】
4.1概述
查遍全网,发现其实也没有一个准确的定义什么是上下文。知乎上有一个通俗的回答:
每一段程序都有很多外部变量。只有像Add这种简单的函数才是没有外部变量的。一旦你的一段程序有了外部变量,这段程序就不完整,不能独立运行。你为了使他们运行,就要给所有的外部变量一个一个写一些值进去。这些值的集合就叫上下文。
– vzch
其实看这个名词的英文写法就知道大概:context
flask 中有两种context:application context
和 request context
,也就是说上下文指的就是这两个封装数据的对象。也许还有一些高深的奥义,但怎么定义不用深究,怎么使用才是关键。
再来梳理下flask中上下文的管理流程:
- 请求进来,创建ctx对象和app_ctx对象
- ctx对象内封装了request和session,app_ctx对象内封装了app和g
- 然后把他们入栈 -->至此,完成“上文”
- 执行视图函数时,可以直接从栈上获取这两个对象中封装的数据
- 完事后,再把ctx和app_ctx销毁 -->至此,就完成“下文”
4.2 LocalStack
在整个流程中的封装数据以及读取数据时,用到了两个东西:LocalStack
和 Local
。它们两个的作用就是让我们可以动态地获取两个上下文的内容,在并发程序中每个视图函数都有属于自己的上下文,而不会出现混乱。
再回看一下ctx.push()
:
def push(self):
app_ctx = _app_ctx_stack.top
if app_ctx is None or app_ctx.app != self.app:
# 创建AppContext对象,里面封装了app对象和g
app_ctx = self.app.app_context() # AppContext()
# app_ctx入栈
app_ctx.push()
self._implicit_app_ctx_stack.append(app_ctx)
else:
self._implicit_app_ctx_stack.append(None)
# 这里_request_ctx_stack是LocalStack()对象,在globals.py中定义的全局对象
_request_ctx_stack.push(self) # 注意传递了self参数
上面两个对象的push()
入栈,追溯源码其实最终都是走到了LocalStack()
对象的push()
方法:
class LocalStack:
# 创建local对象用于存储数据
def __init__(self) -> None:
self._local = Local()
def __release_local__(self) -> None:
self._local.__release_local__()
def __call__(self) -> "LocalProxy":
def _lookup() -> t.Any:
rv = self.top
if rv is None:
raise RuntimeError("object unbound")
return rv
return LocalProxy(_lookup)
# 先将ctx/app_ctx存入一个列表中,再把列表存入local对象中
def push(self, obj: t.Any) -> t.List[t.Any]:
"""Pushes a new item to the stack"""
rv = getattr(self._local, "stack", []).copy()
rv.append(obj)
self._local.stack = rv
return rv
# 从local对象中的列表内取出末尾元素
def pop(self) -> t.Any:
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()
# 从local对象中的列表内取出末尾元素
@property
def top(self) -> t.Any:
try:
return self._local.stack[-1]
except (AttributeError, IndexError):
return None
可以看到,这个LocalStack
类内有push
、pop
和 top
方法,操作的是存储在Local
中的ctx/app_ctx数据。
所以,这个类其实是基于 Local
实现的栈结构。
4.3 Local
class Local:
__slots__ = ("_storage",)
# 设置self._storage为一个ContextVar对象,用于保存数据
def __init__(self) -> None:
object.__setattr__(self, "_storage", ContextVar("local_storage"))
def __iter__(self) -> t.Iterator[t.Tuple[int, t.Any]]:
return iter(self._storage.get({}).items())
# 当调用Local对象时,返回对应的LocalProxy
def __call__(self, proxy: str) -> "LocalProxy":
"""Create a proxy for a name."""
return LocalProxy(self, proxy)
# Local类中特有的方法,用于清空数据
def __release_local__(self) -> None:
self._storage.set({})
def __getattr__(self, name: str) -> t.Any:
values = self._storage.get({})
try:
return values[name]
except KeyError:
raise AttributeError(name) from None
def __setattr__(self, name: str, value: t.Any) -> None:
values = self._storage.get({}).copy() # 从ContextVar对象中取数据
values[name] = value # 编辑数据,如: {"stark":[ctx,]}
self._storage.set(values) # 存入新的数据
def __delattr__(self, name: str) -> None:
values = self._storage.get({}).copy()
try:
del values[name]
self._storage.set(values)
except KeyError:
raise AttributeError(name) from None
可以看到,Local
对象内部的数据都是保存在 __storage__
属性的,它是一个ContextVar对象
NOTE:werkzeug自2.0.0版本后引入的ContextVar
替代了旧版的threading.local
,他是python3.7开始支持的,用于声明一个新的上下文变量(参阅:werkzeug官方文档,contextvars上下文变量)
看一个示例了解用法:
from contextvars import *
storage = ContextVar('local_storage')
x = {"stark": ["aaa", ]}
storage.set(x) # 存入数据
print(storage) # <ContextVar name='local_storage' at 0x000001B23810C7C0>
y1 = storage.get({}) # 取数据
y2 = storage.get() # 同上
print(y1) # {'stark': ['aaa']}
print(y2) # {'stark': ['aaa']}
storage.set({}) # 清空数据
z = storage.get()
print(z) # {}
4.4 LocalProxy
回看LocalStark中,有个__call__
方法,返回的是LocalProxy
对象
def __call__(self) -> "LocalProxy":
def _lookup() -> t.Any:
rv = self.top
if rv is None:
raise RuntimeError("object unbound")
return rv
return LocalProxy(_lookup)
我们说LocalProxy
是Local
对象的代理对象,源码:
class LocalProxy:
__slots__ = ("__local", "__name", "__wrapped__")
def __init__(
self,
local: t.Union["Local", t.Callable[[], t.Any]],
name: t.Optional[str] = None,
) -> None:
object.__setattr__(self, "_LocalProxy__local", local) # 设置self.__local
object.__setattr__(self, "_LocalProxy__name", name) # 设置self.__name
if callable(local) and not hasattr(local, "__release_local__"):
object.__setattr__(self, "__wrapped__", local)
def _get_current_object(self) -> t.Any:
if not hasattr(self.__local, "__release_local__"): # type: ignore
return self.__local() # type: ignore
try:
return getattr(self.__local, self.__name) # type: ignore
except AttributeError:
name = self.__name # type: ignore
raise RuntimeError(f"no object bound to {name}") from None
__doc__ = _ProxyLookup( # type: ignore
class_value=__doc__, fallback=lambda self: type(self).__doc__
)
__repr__ = _ProxyLookup( # type: ignore
repr, fallback=lambda self: f"<{type(self).__name__} unbound>"
)
__str__ = _ProxyLookup(str) # type: ignore
__bytes__ = _ProxyLookup(bytes)
这里实现的关键是把通过参数传递进来的 Local
实例保存在 __local
属性中,并定义了 _get_current_object()
方法获取当前的对象。
NOTE:前面双下划线的私有属性,会保存到 _ClassName__variable
中。所以这里通过 “_LocalProxy__local”
设置的值,后面可以通过 self.__local
来获取。
然后 LocalProxy
重写了所有的魔术方法(名字前后有两个下划线的方法,上面只列出部分),具体操作都是转发给代理对象的。
此时,我们再看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
# context locals
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
current_app: "Flask" = LocalProxy(_find_app)
request: "Request" = LocalProxy(partial(_lookup_req_object, "request"))
session: "SessionMixin" = LocalProxy(partial(_lookup_req_object, "session"))
g: "_AppCtxGlobals" = LocalProxy(partial(_lookup_app_object, "g"))
这里可以看到两个context中封装的对象的提取都是经由代理对象LocalProxy
来转发给Local
执行的。
比如:request=LocalProxy(partial(_lookup_req_object, "request"))
,这里的partial
是偏函数,可以在调用之前为函数提前传参,起到固定参数的作用(不会执行),举个例子:
from functools import partial
def add(a,b,c):
print(a+b+c)
new_func = partial(add,1,2) # 为add函数提前传两个值,成为一个新函数
new_func(3) # 再调用只需要传剩下的值
理解了偏函数,那这里就相当于:
request= LocalProxy(partial(_lookup_req_object, "request"))
# new_func = _lookup_req_object("request") # 不会执行
# 相当于request= LocalProxy(new_func),调用init方法:
class LocalProxy:
def __init__(self,local,name=None): # local = new_func
self.__local = new_func
self.name:None
因此LocalProxy
中的_get_current_object
方法返回的self.__local()
,实际上就是执行new_func()
,最终从ctx
里找到并赋值给request
对象。
# new_func() = :
_lookup_req_object("request")
top = _request_ctx_stack.top
if top is None:
raise RuntimeError(_request_ctx_err_msg)
return getattr(top, "request")
同理,当我们在视图函数里执行print(request)
时,就会调用LocalProxy
里的__str__
,同理当你request.method
的时候,就会调用对应的__getattr__
方法。这就是一个典型的代理模式使用。
至此,上下文中的存取数据的流程就能大概理清楚了。
最后,学以致用,一个简单的例子再来理解下上下文的使用:
from flask import Flask, session, request, current_app, g
app = Flask(__name__,static_url_path='/xx')
@app.route('/index')
def index():
# session, request, current_app, g 本质上全部都是LocalProxy对象。
"""
session['x'] = 123 --> ctx.session['x'] = 123
request.method --> ctx.request.method
current_app.config --> app_ctx.app.config
g.x1 --> app_ctx.g.x1
"""
session['k1'] = 123
print(request.args)
print(request.form)
return 'hello world'
if __name__ == '__main__':
app.run()
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· 字符编码:从基础到乱码解决
· SpringCloud带你走进微服务的世界