文章出处 https://www.cnblogs.com/jackchengcc/archive/2018/11/29/10025949.html
一:什么是上下文
每一段程序都有很多外部变量。只有像Add这种简单的函数才是没有外部变量的。一旦你的一段程序有了外部变量,这段程序就不完整,不能独立运行。你为了使他们运行,就要给所有的外部变量一个一个写一些值进去。这些值的集合就叫上下文。
在 flask 中,视图函数需要知道它执行情况的请求信息(请求的 url,参数,方法等)以及应用信息(应用中初始化的数据库等),才能够正确运行。最直观地做法是把这些信息封装成一个对象,作为参数传递给视图函数。但是这样的话,所有的视图函数都需要添加对应的参数,即使该函数内部并没有使用到它。flask 的做法是把这些信息作为类似全局变量的东西,视图函数需要的时候,可以使用 from flask import request
获取。但是这些对象和全局变量不同的是——它们必须是动态的,因为在多线程或者多协程的情况下,每个线程或者协程获取的都是自己独特的对象,不会互相干扰。
二:实现过程
在python多线程中,有threading.local,可以实现多个线程访问某个变量时,每个线程只能看到自己的数据(flask上下文中,每个线程也只能访问自己请求所封装的数据),其内部原理大致为:封装的对象有一个字典,字典中保存了每个线程id所对应的数据,读取到该对象时,它动态的查询当前线程id对应的数据。代码实现原理大致如下:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
|
flask 中有两种上下文:application context
和 request context
。上下文有关的内容定义在 globals.py
文件,文件的内容也非常短:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
flask
提供两种上下文:application context
和 request context
。application context
又演化出来两个变量 current_app
和 g
,而 request context
则演化出来 request
和 session
。
这里的实现用到了两个东西:LocalStack
和 LocalProxy
。它们两个的结果就是我们可以动态地获取两个上下文的内容,在并发程序中每个视图函数都会看到属于自己的上下文,而不会出现混乱。
LocalStack
和 LocalProxy
都是 werkzeug
提供的,定义在 local.py
文件中。在分析这两个类之前,我们先介绍这个文件另外一个基础的类 Local
。Local
就是实现了类似 threading.local
的效果——多线程或者多协程情况下全局变量的隔离效果。下面是它的代码:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
|
可以看到,Local
对象内部的数据都是保存在 __storage__
属性的,这个属性变量是个嵌套的字典:map[ident]map[key]value
。最外面字典 key 是线程或者协程的 identity,value 是另外一个字典,这个内部字典就是用户自定义的 key-value 键值对。用户访问实例的属性,就变成了访问内部的字典,外面字典的 key 是自动关联的。__ident_func
是 协程的 get_current
或者线程的 get_ident
,从而获取当前代码所在线程或者协程的 id。
除了这些基本操作之外,Local
还实现了 __release_local__
,用来清空(析构)当前线程或者协程的数据(状态)。__call__
操作来创建一个 LocalProxy
对象,LocalProxy
会在下面讲到。
理解了 Local
,我们继续回来看另外两个类。
LocalStack
是基于 Local
实现的栈结构。如果说 Local
提供了多线程或者多协程隔离的属性访问,那么 LocalStack
就提供了隔离的栈访问。下面是它的实现代码,可以看到它提供了 push
、pop
和 top
方法。
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
|
我们在之前看到了 request context
的定义,它就是一个 LocalStack
的实例:
?
1 |
|
它会当前线程或者协程的请求都保存在栈里,等使用的时候再从里面读取。至于为什么要用到栈结构,而不是直接使用 Local
,我们会在后面揭晓答案,你可以先思考一下。
LocalProxy
是一个 Local
对象的代理,负责把所有对自己的操作转发给内部的 Local
对象。LocalProxy
的构造函数介绍一个 callable 的参数,这个 callable 调用之后需要返回一个 Local
实例,后续所有的属性操作都会转发给 callable 返回的对象。
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
|
这里实现的关键是把通过参数传递进来的 Local
实例保存在 __local
属性中,并定义了 _get_current_object()
方法获取当前线程或者协程对应的对象。
NOTE:前面双下划线的属性,会保存到 _ClassName__variable
中。所以这里通过 “_LocalProxy__local”
设置的值,后面可以通过 self.__local
来获取。关于这个知识点,可以查看 stackoverflow 的这个问题。
然后 LocalProxy
重写了所有的魔术方法(名字前后有两个下划线的方法),具体操作都是转发给代理对象的。这里只给出了几个魔术方法,感兴趣的可以查看源码中所有的魔术方法。
继续回到 request context
的实现:
?
1 2 3 |
|
再次看这段代码希望能看明白,_request_ctx_stack
是多线程或者协程隔离的栈结构,request
每次都会调用 _lookup_req_object
栈头部的数据来获取保存在里面的 requst context
。
那么请求上下文信息是什么被放在 stack 中呢?还记得之前介绍的 wsgi_app()
方法有下面两行代码吗?
?
1 2 |
|
每次在调用 app.__call__
的时候,都会把对应的请求信息压栈,最后执行完请求的处理之后把它出栈。
我们来看看request_context
, 这个 方法只有一行代码:
?
1 2 |
|
它调用了 RequestContext
,并把 self
和请求信息的字典 environ
当做参数传递进去。追踪到 RequestContext
定义的地方,它出现在 ctx.py
文件中,代码如下:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
|
每个 request context 都保存了当前请求的信息,比如 request 对象和 app 对象。在初始化的最后,还调用了 match_request
实现了路由的匹配逻辑。
push
操作就是把该请求的 ApplicationContext
(如果 _app_ctx_stack
栈顶不是当前请求所在 app ,需要创建新的 app context) 和 RequestContext
有关的信息保存到对应的栈上,压栈后还会保存 session 的信息; pop
则相反,把 request context 和 application context 出栈,做一些清理性的工作。
到这里,上下文的实现就比较清晰了:每次有请求过来的时候,flask 会先创建当前线程或者进程需要处理的两个重要上下文对象,把它们保存到隔离的栈里面,这样视图函数进行处理的时候就能直接从栈上获取这些信息。
NOTE:因为 app 实例只有一个,因此多个 request
共享了 application context
。
到这里,关于 context 的实现和功能已经讲解得差不多了。还有两个疑惑没有解答。
- 为什么要把 request context 和 application context 分开?每个请求不是都同时拥有这两个上下文信息吗?
- 为什么 request context 和 application context 都有实现成栈的结构?每个请求难道会出现多个 request context 或者 application context 吗?
第一个答案是“灵活度”,第二个答案是“多 application”。虽然在实际运行中,每个请求对应一个 request context 和一个 application context,但是在测试或者 python shell 中运行的时候,用户可以单独创建 request context 或者 application context,这种灵活度方便用户的不同的使用场景;而且栈可以让 redirect 更容易实现,一个处理函数可以从栈中获取重定向路径的多个请求信息。application 设计成栈也是类似,测试的时候可以添加多个上下文,另外一个原因是 flask 可以多个 application 同时运行:
?
1 2 3 4 5 6 7 |
|
这个例子就是使用 werkzeug
的 DispatcherMiddleware
实现多个 app 的分发,这种情况下 _app_ctx_stack
栈里会出现两个 application context。
Update: 为什么要用 LocalProxy
为什么要使用 LocalProxy
?不使用 LocalProxy
直接访问 LocalStack
的对象会有什么问题吗?
首先明确一点,Local
和 LocalStack
实现了不同线程/协程之间的数据隔离。在为什么用 LocalStack
而不是直接使用 Local
的时候,我们说过这是因为 flask 希望在测试或者开发的时候,允许多 app 、多 request 的情况。而 LocalProxy
也是因为这个才引入进来的!
我们拿 current_app = LocalProxy(_find_app)
来举例子。每次使用 current_app
的时候,他都会调用 _find_app
函数,然后对得到的变量进行操作。
如果直接使用 current_app = _find_app()
有什么区别呢?区别就在于,我们导入进来之后,current_app
就不会再变化了。如果有多 app 的情况,就会出现错误,比如:
?
1 2 3 4 5 6 7 8 9 10 |
|
这里我们出现了嵌套的 app,每个 with 上下文都需要操作其对应的 app
,如果不适用 LocalProxy
是做不到的。
对于 request
也是类似!但是这种情况真的很少发生,有必要费这么大的功夫增加这么多复杂度吗?
其实还有一个更大的问题,这个例子也可以看出来。比如我们知道 current_app
是动态的,因为它背后对应的栈会 push 和 pop 元素进去。那刚开始的时候,栈一定是空的,只有在 with app.app_context()
这句的时候,才把栈数据 push 进去。而如果不采用 LocalProxy
进行转发,那么在最上面导入 from flask import current_app
的时候,current_app
就是空的,因为这个时候还没有把数据 push 进去,后面调用的时候根本无法使用。
所以为什么需要 LocalProxy
呢?简单总结一句话:因为上下文保存的数据是保存在栈里的,并且会动态发生变化。如果不是动态地去访问,会造成数据访问异常。
Flask上下文流程图:
原文地址:https://www.cnblogs.com/AbnerLc/p/11637119.html