flask中的上下文分两种,application context和request context,即应用上下文和请求上下文。
从名字上看,可能会有误解,认为应用上下文是一个应用的全局变量,所有请求都可以访问修改其中的内容;而请求上下文则是请求内可访问的内容。
但事实上,这两者并不是全局与局部的关系,它们都处于一个请求的局部中。
先说结论:每个请求的g都是独立的,并且在整个请求内都是可访问修改的。
下面来研究一下。
上下文类的定义:
上下文类定义在flask.ctx模块中
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()
# Like request context, app contexts can be pushed multiple times
# but there a basic "refcount" is enough to track them.
self._refcnt = 0
查看了源代码,AppContext类即是应用上下文,可以看到里面只保存了几个变量,其中比较重要的有:
app是当前web应用对象的引用,如Flask;还有g,用来保存需要在每个请求中需要用到的请求内全局变量。
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
# Request contexts can be pushed multiple times and interleaved with
# other request contexts. Now only if the last level is popped we
# get rid of them. Additionally if an application context is missing
# one is created implicitly so for each level we add this information
self._implicit_app_ctx_stack = []
RequestContext即请求上下文,其中有我们熟悉的request和session,app和应用上下文中的app含义相同。
上下文对象的作用域
那么这两种上下文运行时是怎么被使用的呢?
线程有个叫做ThreadLocal的类,也就是通常实现线程隔离的类。而werkzeug自己实现了它的线程隔离类:werkzeug.local.Local。LocalStack就是用Local实现的。
在flask.globals模块中定义了两个LocalStack对象:
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
LocalStack是flask定义的线程隔离的栈存储对象,分别用来保存应用和请求上下文。
它是线程隔离的意思就是说,对于不同的线程,它们访问这两个对象看到的结果是不一样的、完全隔离的。这是根据pid的不同实现的,类似于门牌号。
而每个传给flask对象的请求,都是在不同的线程中处理,而且同一时刻每个线程只处理一个请求。所以对于每个请求来说,它们完全不用担心自己上下文中的数据被别的请求所修改。
然后就可以解释这个特性:从flask模块中引入的g、session、request、current_app是怎么做到同一个对象能在所有请求中使用并且不会冲突。
这几个对象还是定义在flask.globals中:
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'))
LocalProxy类的构造函数接收一个callable参数,上面这几个就传入了一个偏函数。以g为例,当对g进行操作时,就会调用作为参数的偏函数,并把操作转发到偏函数返回的对象上。
查看这几个函数的实现:
def _lookup_req_object(name):
top = _request_ctx_stack.top
if top is None:
raise RuntimeError('working outside of request context')
return getattr(top, name)
def _lookup_app_object(name):
top = _app_ctx_stack.top
if top is None:
raise RuntimeError('working outside of application context')
return getattr(top, name)
def _find_app():
top = _app_ctx_stack.top
if top is None:
raise RuntimeError('working outside of application context')
return top.app
由于_app_ctx_stack和_request_ctx_stack都是线程隔离的,所以对g的调用就是这样一个过程:
访问g-->从当前线程的应用上下文栈顶获取应用上下文-->取出其中的g对象-->进行操作。
所以可以通过一个g对象而让所有线程互不干扰的访问自己的g。
上下文对象的推送
构建Flask对象后并不会推送上下文,而在Flask对象调用run()作为WSGI 应用启动后,每当有请求进入时,在推送请求上下文前,如果有必要就会推送应用上下文。但运行了run就会阻塞程序,所以在shell中调试时,必须手动推送上下文;或者使用flask-scripts,它运行的任务会在开始时自动推送。
上面加粗的“如果有必要”,那么什么叫有必要呢?是不是意味着在每个线程里应用上下文只会被推送一次、一次请求结束下一次请求来的时候就不用再推送应用上下文了呢?
来看RequestContext的源码,push函数:
def push(self):
top = _request_ctx_stack.top
if top is not None and top.preserved:
top.pop(top._preserved_exc)
# Before we push the request context we have to ensure that there
# is an application context.
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)
flask在推送请求上下文的时候调用push函数,他会检查当前线程的应用上下文栈顶是否有应用上下文;如果有,判断与请求上下文是否属于同一个应用。在单WSGI应用的程序中,后者的判断无意义。
此时,只要没有应用上下文就会推送一个当前应用的上下文,并且把该上下文记录下来。
请求处理结束,调用auto_pop函数,其中又调用自身的pop函数:
def pop(self, exc=None):
app_ctx = self._implicit_app_ctx_stack.pop()
………………
………………
rv = _request_ctx_stack.pop()
# Get rid of the app as well if necessary.
if app_ctx is not None:
app_ctx.pop(exc)
会把请求上下文和应用上下文都pop掉。
故,在单WSGI应用环境下,每个请求的两个上下文都是完全独立的(独立于线程上曾经的请求,独立于其他线程的请求)。Q.E.D
那么,什么时候没必要推送呢?事实上,每次请求到来的时候都会推送,都是有必要的。因为当Flask在作为WSGI应用运行的时候,不可能出现当前线程的应用上下文已存在的情况。
那么就要搞清什么时候会有已存在的应用上下文。
研究时我参考了博文:https://blog.tonyseek.com/post/the-context-mechanism-of-flask/
该博文在最后提到了“两个疑问”:①应用和请求上下文在运行时都是线程隔离的,为何要分开来?②每个线程同时只处理一个请求,上下文栈肯定只有一个对象,为何要用栈来存储?
博主认为,这两个设计都是为了在离线状态下调试用:
所以,综上所述,在非离线状态下,上下文栈在每个WSGI应用里是独立的,而每个应用里线程同时只处理一个请求,故上下文栈肯定只有一个对象。并且,在请求结束后都会释放,所以新的请求来的时候都会重新推送两个上下文。
小结:
解释了这么多,对于flask编程来说,只有一个应用上的结论:每个请求的g都是独立的,并且在整个请求内都是可访问修改的。
一、threading-local
1、threding-local
作用:为每一个线程开辟一块空间进行数据存储 from threading import local from threading import Thread import time # 示例化local对象 ret=local() def task(s): global ret ret.value=s time.sleep(2) print(ret.value) # 开启10个线程 for i in range(10): t=Thread(target=task,args=(i,)) t.start()
2、自定义local
# 如果有协程则使用协程唯一标识getcurrent try: from greenlet import getcurrent as get_ident except Exception as e: from threading import Thread,get_ident class Local(object): # 线程的唯一标识 ident = get_ident() def __init__(self): # 执行父类__setattr__ object.__setattr__(self,"storage",{}) def __setattr__(self,k, v): """ 构造dict storage={ ident:{val:0}, ident:{val:1}, ident:{val:3}, ident:{val:4}, } """ if self.ident in self.storage: self.storage[self.ident][k] = v else: self.storage[self.ident] = {k: v} def __getattr__(self,k): return self.storage[self.ident][k] obj=Local() def task(arg): # 执行__setattr__ obj.var=arg # 执行__getattr__ v=obj.var print(v) for i in range(10): t = Thread(target=task,args=(i,)) t.start()
二、上下文管理源码分析
1、上下文管理本质(类似于threading.local) 1、每一个线程都会在Local类中创建一条数据 { “唯一标识”:{stark:[ctx,]} “唯一标识”:{stark:[ctx,]} } 2、当请求进来之后,将请求相关数据添加到列表里面[request,],以后如果使用时,就去读取 3、列表中的数据,请求完成之后,将request从列表中移除 2、在源码中分析上下文管理 第一阶段:执行__call__--->app.wsgi-->将ctx(request,session)封装为RequestContent()在(open_session), app_ctx(g,app)封装为APPContent()通过LocalStack将这两个类放入Local对象中 第二阶段:视图函数导入:request/session/g/app ,通过偏函数(_lookup_req_object)在通过(LocalProxy())去LocalStack中的Local类中对其进行增删改查操作 第三阶段:请求处理完毕 - 通过save_session将签名session保存到cookie -通过ctx.pop()去LocalStack中的Local类- 将ctx删除
有关面试问题
问题一:flask和django的区别: 对于django来说,内部组件特别多,自身功能强大,有点大而全,而flask,内置组件很少,但是它的第三方组件很多,扩展性强,有点短小精悍,而它们之间也有相似之处, 因为它们两个框架都没有写sockte,都是基于wsgi协议做的,在此之外,flask框架中的上下文管理较为耀眼。 相同点:它们两个框架都没有写sockte,都是基于wsgi协议做的 请求相关数据传递的方式不同:django:通过传递request参数取值 flask:见问题二 组件不同:django组件多 flask组件少,第三方组件丰富 问题1.1: flask上下文管理: 简单来说,falsk上下文管理可以分为三个阶段: 1、请求进来时,将请求相关的数据放入上下问管理中 2、在视图函数中,要去上下文管理中取值 3、请求响应,要将上下文管理中的数据清除 详细点来说: 1、请求刚进来,将request,session封装在RequestContext类中,app,g封装在AppContext类中,并通过LocalStack将requestcontext和appcontext放入Local类中 2、视图函数中,通过localproxy--->偏函数--->localstack--->local取值 3、请求相应时,先执行save.session()再各自执行pop(),将local中的数据清除 问题1.2 flask第三方组件 flask: -flask-session 默认放入cookie,可以放入redis -flask-redis -flask-migrate -flask-script -blinker 信号 公共: DBUtils 数据库连接池 wtforms 表单验证+生成HTML标签 sqlalchemy 自定义:Auth 参考falsk-login 问题二:Flask中的session是什么时候创建,什么时候销毁的? 当请求进来时,会将requset和session封装为一个RequestContext对象,通过LocalStack将RequestContext放入到Local对象中,因为 请求第一次来session是空值,所以执行open_session,给session(uuid4())赋值,再通过视图函数处理,请求响应时执行save.session,将签名session写入cookie中,再讲Local中的数值pop掉。 问题三:flask中一共有几个LocalStack和Local对象 两个LocalStack,两个Local request、session共同用一个LocalStack和Local g、app共同用一个Localstack和Local 问题四: 为什么把请求放到RequestContext中: 因为request和session都是在视图中操作频繁的数据,也是用户请求需要用的数据,将request和session封装在RequestContext中top,pop一次就可以完成,而单独不封装在一起就会多次操作, ctx = RequestContext(request,session) 问题五:local作用 -保存 请求上下文对象和app上下文对象 -localstack的源码与threading.local(线程处理)作用相似,不同之处是Local是通过greenlet(协程)获取唯一标识,粒度更细 问题六:Localstack作用 2、将local对象中的数据维护成一个栈【ctx,ctx】(先进后出) { “协程或线程的唯一标识”: { stack:[ctx,ctx,ctx,] }
}
为什么维护成一个栈?
当是web应用时:不管是单线程还是多线程,栈中只有一个数据
- 服务端单线程:
{
111:{stack: [ctx, ]}
}
- 服务端多线程:
{
111:{stack: [ctx, ]}
112:{stack: [ctx, ]}
}
离线脚本:可以在栈中放入多个数据
with app01.app_context():
print(current_app)
with app02.app_context():
print(current_app)
print(current_app)
问题七:什么是g?
g 相当于一次请求的全局变量,当请求进来时将g和current_app封装为一个APPContext类,在通过LocalStack将Appcontext放入Local中,取值时通过偏函数,LocalStack、loca l中取值,响应时将local中的g数据删除:
问题八:怎么获取Session/g/current_app/request
通过 、偏函数(lookup_req_object)、Localstack、Local取值
问题九: 技术点:
- 反射 (LocalProxy())
- 面向对象,封装:RequestContext
- 线程(threading.local)
- 笔试:自己写一个类+列表 实现栈。(LocalStack)
问题十:python基本哪些内容比较重要:
1、反射
-CBV
-django配置文件
-wtforms中的Form()示例化中 将"_fields中的数据封装到From类中"
2、装饰器 (迭代器,生成器)
-flask:路由、装饰器
-认证
-csrf
3、面向对象
-继承、封装、多态(简单描述)
-双下划线:
__mro__ wtform中 FormMeta中继承类的优先级
__dict__
__new__ ,实例化但是没有给当前对象
wtforms,字段实例化时返回:不是StringField,而是UnboundField
rest frawork many=Turn 中的序列化
__call__
flask 请求的入口app.run()
字段生成标签时:字段.__str__ => 字段.__call__ => 插件.__call__
__iter__ 循环对象是,自定义__iter__
wtforms中BaseForm中循环所有字段时定义了__iter__
metaclass
- 作用:用于指定当前类使用哪个类来创建
- 场景:在类创建之前定制操作
示例:wtforms中,对字段进行排序。