https://zhuanlan.zhihu.com/p/26097310

请求上下文

使用目的

  • 在Flask中处理请求时,应用会生成一个“请求上下文”对象。

  • 整个请求的处理过程,都会在这个上下文对象中进行。这保证了请求的处理过程不被干扰

  • 处理请求的具体代码如下:

    1
    2
    3
    4
    5
    def wsgi_app(self, environ, start_response):
    with self.request_context(environ):
    # with语句中完成请求处理 , 生成一个`response`对象
    ...
    return response(environ, start_response)

LocalStack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> from werkzeug.local import LocalStack
>>> import threading

# 创建一个`LocalStack`对象
>>> local_stack = LocalStack()
# 查看local_stack中存储的信息
>>> local_stack._local.__storage__
{}

# 定义一个函数,这个函数可以向`LocalStack`中添加数据
>>> def worker(i):
local_stack.push(i)

# 使用3个线程运行函数`worker`
>>> for i in range(3):
t = threading.Thread(target=worker, args=(i,))
t.start()

# 再次查看local_stack中存储的信息
>>> local_stack._local.__storage__
{<greenlet.greenlet at 0x4bee5a0>: {'stack': [2]},
<greenlet.greenlet at 0x4bee638>: {'stack': [1]},
<greenlet.greenlet at 0x4bee6d0>: {'stack': [0]}
}
  • 存储在LocalStack中的信息以字典的形式存在:键为线程/协程的标识数值,值也是字典形式。每当有一个线程/协程上要将一个对象push进LocalStack栈中,会形成如上一个“键-值”对。
  • 这样的一种结构很好地实现了线程/协程的隔离,每个线程/协程都会根据自己线程/协程的标识数值确定存储在栈结构中的值。
  • LocalStack还实现了push、pop、top等方法。其中top方法永远指向栈顶的元素。栈顶的元素是指当前线程/协程中最后被推入栈中的元素,即local_stack._local.stack[-1](注意,是stack键对应的对象中最后被推入的元素)。

请求上下文

请求上下文0.1版本

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
class _RequestContext(object):
"""请求上下文(request context)包含所有请求相关的信息。它会在请求进入时被创建,
然后被推送到_request_ctx_stack,在请求结束时会被相应的移除。它会为提供的
WSGI环境创建URL适配器(adapter)和请求对象。
"""
# 会在flask.Flask.request_context和flask.Flask.test_requset_context方法中
# 调用,以便生成请求上下文。
def __init__(self, app, environ):
self.app = app
self.url_adapter = app.url_map.bind_to_environ(environ) # 绑定了当前环境信息,用于构建URL,在url_for函数中使用
self.request = app.request_class(environ) # 创建请求对象,包含请求信息
self.session = app.open_session(self.request) # 创建session对象,用于存储用户会话数据到cookie中
self.g = _RequestGlobals() # 创建g对象,用于在当前请求存储全局变量
self.flashes = None # 存储当前请求的通过flash函数发送的消息

def __enter__(self):
_request_ctx_stack.push(self) # 将当前请求上下文对象推送到_request_ctx_stack堆栈,这个堆栈在最后定义

def __exit__(self, exc_type, exc_value, tb):
# 在调试模式(debug mode)而且有异常发生时,不要移除(pop)请求堆栈。
# 这将允许调试器(debugger)在交互式shell中仍然可以获取请求对象。
if tb is None or not self.app.debug:
_request_ctx_stack.pop()

_request_ctx_stack = LocalStack()
  • “请求上下文”是一个上下文对象,实现了__enter____exit__方法。可以使用with语句构造一个上下文环境。
  • 进入上下文环境时,_request_ctx_stack这个栈中会推入一个_RequestContext对象。这个栈结构就是上面讲的LocalStack栈。
  • 推入栈中的_RequestContext对象有一些属性,包含了请求的的所有相关信息。例如app、request、session、g、flashes。还有一个url_adapter,这个对象可以进行URL匹配。
  • 在with语句构造的上下文环境中可以进行请求处理。当退出上下文环境时,_request_ctx_stack这个栈会销毁刚才存储的上下文对象。

以上的运行逻辑使得请求的处理始终在一个上下文环境中,这保证了请求处理过程不被干扰,而且请求上下文对象保存在LocalStack栈中,也很好地实现了线程/协程的隔离。

![Untitled Diagram](/images/Untitled Diagram.jpg)

以下是一个简单的例子:

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
>>> from flask import Flask, _request_ctx_stack
>>> import threading
>>> app = Flask(__name__)
# 先观察_request_ctx_stack中包含的信息
>>> _request_ctx_stack._local.__storage__
{}

# 创建一个函数,用于向栈中推入请求上下文
# 本例中不使用`with`语句
>>> def worker():
# 使用应用的test_request_context()方法创建请求上下文
request_context = app.test_request_context()
_request_ctx_stack.push(request_context)

# 创建3个进程分别执行worker方法
>>> for i in range(3):
t = threading.Thread(target=worker)
t.start()

# 再观察_request_ctx_stack中包含的信息
>>> _request_ctx_stack._local.__storage__
{<greenlet.greenlet at 0x5e45df0>: {'stack': [<flask._RequestContext at 0x710c668>]},
<greenlet.greenlet at 0x5e45e88>: {'stack': [<flask._RequestContext at 0x7107f28>]},
<greenlet.greenlet at 0x5e45f20>: {'stack': [<flask._RequestContext at 0x71077f0>]}
}
  • _request_ctx_stack中为每一个线程创建了一个“键-值”对,
  • 每一“键-值”对中包含一个请求上下文对象。
  • 如果使用with语句,在离开上下文环境时栈中销毁存储的上下文对象信息。

请求上下文0.9版本

  • 在0.9版本中,Flask引入了“应用上下文”的概念,这对“请求上下文”的实现有一定的改变。
  • 这个版本的“请求上下文”也是一个上下文对象。在使用with语句进入上下文环境后,_request_ctx_stack会存储这个上下文对象。不过与0.1版本相比,有以下几点改变:
    • 请求上下文实现了push、pop方法,这使得对于请求上下文的操作更加的灵活;
    • 伴随着请求上下文对象的生成并存储在栈结构中,Flask还会生成一个“应用上下文”对象,而且“应用上下文”对象也会存储在另一个栈结构中去。这是两个版本最大的不同。
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 v0.9
def push(self):
"""将请求上下文绑定到当前上下文。"""
# top:最顶层的请求上下文对象
top = _request_ctx_stack.top
if top is not None and top.preserved:
top.pop()

# 在推送请求上下文之前,我们必须确保存在应用程序上下文。
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)
self.session = self.app.open_session(self.request)
if self.session is None:
self.session = self.app.make_null_session()


def pop(self, exc=None):
"""弹出请求上下文并通过此操作解除绑定。这还将触发执行由:
meth:`~flask.flask.teardown_request`装饰器注册的函数。
"""
app_ctx = self._implicit_app_ctx_stack.pop()

clear_request = False
if not self._implicit_app_ctx_stack:
self.preserved = False
if exc is None:
exc = sys.exc_info()[1]
self.app.do_teardown_request(exc)
clear_request = True

rv = _request_ctx_stack.pop()
assert rv is self, 'Popped wrong request context. (%r instead of %r)' \
% (rv, self)

# 在请求结束时去掉循环依赖项,这样我们就不需要GC处于活动状态。
if clear_request:
rv.request.environ['werkzeug.request'] = None

# 如果有必要的话,也把应用程序扔掉。
if app_ctx is not None:
app_ctx.pop(exc)
  • push() : 当要将一个“请求上下文”推入_request_ctx_stack栈中的时候,会先检查另一个栈_app_ctx_stack的栈顶是否存在“应用上下文”对象或者栈顶的“应用上下文”对象的应用是否是当前应用。如果不存在或者不是当前对象,Flask会自动先生成一个“应用上下文”对象,并将其推入_app_ctx_stack中。
  • pop() : 到当要离开“请求上下文”环境的时候,Flask会先将“请求上下文”对象从_request_ctx_stack栈中销毁,之后会根据实际的情况确定销毁“应用上下文”对象。

例子说明 :

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
>>> from flask import Flask, _request_ctx_stack, _app_ctx_stack
>>> app = Flask(__name__)

# 先检查两个栈的内容
>>> _request_ctx_stack._local.__storage__
{}
>>> _app_ctx_stack._local.__storage__
{}

# 生成一个请求上下文对象
>>> request_context = app.test_request_context()
>>> request_context.push()

# 请求上下文推入栈后,再次查看两个栈的内容
>>> _request_ctx_stack._local.__storage__
{<greenlet.greenlet at 0x6eb32a8>: {'stack': [http://localhost/' [GET] of __main__>]}}
>>> _app_ctx_stack._local.__storage__
{<greenlet.greenlet at 0x6eb32a8>: {'stack': [<flask.ctx.AppContext at 0x5c96a58>]}}

>>> request_context.pop()

# 销毁请求上下文时,再次查看两个栈的内容
>>> _request_ctx_stack._local.__storage__
{}
>>> _app_ctx_stack._local.__storage__
{}

应用上下文

上部分中简单介绍了“应用上下文”和“请求上下文”的关系。那什么是“应用上下文”呢?我们先看一下它的类:

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
class AppContext(object):
"""
应用上下文将应用对象隐式绑定到当前线程或greenlet,
其做法类似于`request context`类绑定请求信息的方式。
如果创建了请求上下文,但应用程序不在单个应用程序上下文的顶部,
则也会隐式创建应用程序上下文。
"""

def __init__(self, app):
self.app = app
self.url_adapter = app.create_url_adapter(None)

# 与请求上下文一样,应用上下文可以被多次推送,
# 但有一个基本的“refcount”(引用计数)跟踪它们。
self._refcnt = 0

def push(self):
"""将应用程序上下文绑定到当前上下文。"""
self._refcnt += 1
_app_ctx_stack.push(self)

def pop(self, exc=None):
"""弹出应用程序上下文"""
self._refcnt -= 1
if self._refcnt <= 0:
if exc is None:
exc = sys.exc_info()[1]
self.app.do_teardown_appcontext(exc)
rv = _app_ctx_stack.pop()
assert rv is self, 'Popped wrong app context. (%r instead of %r)' % (rv, self)

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

def __exit__(self, exc_type, exc_value, tb):
self.pop(exc_value)
  • 应用上下文也是一个上下文对象,可以使用with语句构造一个上下文环境,它也实现了push、pop等方法。
  • “应用上下文”的构造函数也和“请求上下文”类似,都有app、url_adapter等属性。
  • “应用上下文”存在的一个主要功能就是确定请求所在的应用

既然“请求上下文”中也包含app等和当前应用相关的信息,那么只要调用_request_ctx_stack.top.app或者魔法current_app就可以确定请求所在的应用了,那为什么还需要“应用上下文”对象呢?

  • 对于单应用单请求来说,使用“请求上下文”确实就可以了。
  • 然而,Flask的设计理念之一就是多应用的支持。当在一个应用的请求上下文环境中,需要嵌套处理另一个应用的相关操作时,“请求上下文”显然就不能很好地解决问题了。
  • 如何让请求找到“正确”的应用呢?我们可能会想到,可以再增加一个请求上下文环境,并将其推入_request_ctx_stack栈中。由于两个上下文环境的运行是独立的,不会相互干扰,所以通过调用_request_ctx_stack.top.app或者魔法current_app也可以获得当前上下文环境正在处理哪个应用。这种办法在一定程度上可行,但是如果对于第二个应用的处理不涉及到相关请求,那也就无从谈起“请求上下文”。
  • 为了应对这个问题,Flask中将应用相关的信息单独拿出来,形成一个“应用上下文”对象。
  • 这个对象可以和“请求上下文”一起使用,也可以单独拿出来使用
  • 不过有一点需要注意的是:在创建“请求上下文”时一定要创建一个“应用上下文”对象。有了“应用上下文”对象,便可以很容易地确定当前处理哪个应用,这就是魔法current_app。
  • 在0.1版本中,current_app是对_request_ctx_stack.top.app的引用,而在0.9版本中current_app是对_app_ctx_stack.top.app的引用。

下面以一个多应用的例子进行说明:

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
# example - Flask v0.9
>>> from flask import Flask, _request_ctx_stack, _app_ctx_stack

# 创建两个Flask应用
>>> app = Flask(__name__)
>>> app2 = Flask(__name__)

# 先查看两个栈中的内容
>>> _request_ctx_stack._local.__storage__
{}
>>> _app_ctx_stack._local.__storage__
{}

# 构建一个app的请求上下文环境,
>>> with app.test_request_context():
print "Enter app's Request Context:"
print _request_ctx_stack._local.__storage__
print _app_ctx_stack._local.__storage__
print
# 在这个环境中运行app2的相关操作
with app2.app_context():
print "Enter app2's App Context:"
print _request_ctx_stack._local.__storage__
print _app_ctx_stack._local.__storage__
print
# do something

print "Exit app2's App Context:"
print _request_ctx_stack._local.__storage__
print _app_ctx_stack._local.__storage__
print

# Result
Enter app's Request Context:
{<greenlet.greenlet object at 0x000000000727A178>: {'stack': [http://localhost/' [GET] of __main__>]}}
{<greenlet.greenlet object at 0x000000000727A178>: {'stack': [<flask.ctx.AppContext object at 0x0000000005DD0DD8>]}}

Enter app2's App Context:
{<greenlet.greenlet object at 0x000000000727A178>: {'stack': [http://localhost/' [GET] of __main__>]}}
{<greenlet.greenlet object at 0x000000000727A178>: {'stack': [<flask.ctx.AppContext object at 0x0000000005DD0DD8>, <flask.ctx.AppContext object at 0x0000000007313198>]}}

Exit app2's App Context
{<greenlet.greenlet object at 0x000000000727A178>: {'stack': [http://localhost/' [GET] of __main__>]}}
{<greenlet.greenlet object at 0x000000000727A178>: {'stack': [<flask.ctx.AppContext object at 0x0000000005DD0DD8>]}}
  • 首先创建了两个Flask应用app和app2;
  • 接着我们构建了一个app的请求上下文环境。当进入这个环境中时,这时查看两个栈的内容,发现两个栈中已经有了当前请求的请求上下文对象和应用上下文对象。并且栈顶的元素都是app的请求上下文和应用上下文;
  • 之后,我们再在这个环境中嵌套app2的应用上下文。当进入app2的应用上下文环境时,两个上下文环境便隔离开来,此时再查看两个栈的内容,发现_app_ctx_stack中推入了app2的应用上下文对象,并且栈顶指向它。这时在app2的应用上下文环境中,current_app便会一直指向app2;
  • 当离开app2的应用上下文环境,_app_ctx_stack栈便会销毁app2的应用上下文对象。这时查看两个栈的内容,发现两个栈中只有app的请求的请求上下文对象和应用上下文对象。
  • 最后,离开app的请求上下文环境后,两个栈便会销毁app的请求的请求上下文对象和应用上下文对象,栈为空。

与上下文对象有关的“全局变量”

在Flask中,为了更加方便地处理一些变量,特地提出了“全局变量”的概念。这些全局变量有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Flask v0.9
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_object, 'request'))
session = LocalProxy(partial(_lookup_object, 'session'))
g = LocalProxy(partial(_lookup_object, 'g'))

# 辅助函数
def _lookup_object(name):
"""从请求栈栈顶的请求上下文对象获取name"""
top = _request_ctx_stack.top
if top is None:
raise RuntimeError('working outside of request 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
  • 可以看出,Flask中使用的一些“全局变量”,包括current_app、request、session、g等都来自于上下文对象

  • 其中current_app一直指向_app_ctx_stack栈顶的“应用上下文”对象,是对当前应用的引用。

  • 而request、session、g等一直指向_request_ctx_stack栈顶的“请求上下文”对象,分别引用请求上下文的request、session和g。不过,从 Flask 0.10 起,对象 g 存储在应用上下文中而不再是请求上下文中。

  • 在形成这些“全局变量”的时候,使用了werkzeug.local模块的LocalProxy类。

  • 之所以要用该类,主要是为了动态地实现对栈顶元素的引用。如果不使用这个类,在生成上述“全局变量”的时候,它们因为指向栈顶元素,而栈顶元素此时为None,所以这些变量也会被设置为None常量。后续即使有上下文对象被推入栈中,相应的“全局变量”也不会发生改变。为了动态地实现对栈顶元素的引用,这里必须使用werkzeug.local模块的LocalProxy类。

test_request_context() 和 app_context()

  • 创建请求上下文对象RequestContext 和 创建应用上下文对象AppContext
  • 如果使用with语句的话就会将这个上下文对象推入请求栈/应用栈