Scu_laji

从源码层面理解flask request

从源码层次理解 flask request

flask 是一个很棒的 web 框架,在经历了一些项目之后,我尤其喜欢它的这一特性,无论在什么地方,需要拿到当前请求的东西时。我们只需要这样

1
2
3
4
5
6
7
8
9
from flask import Flask,request
app = Flask()
@app.route('/)
def index():
# 从当前request获取内容
request.args
request.forms
request.cookies
request.values

非常好记且易于理解,但在这背后其实并不简单,是经过一系列复杂的处理之后,我们才能这么愉悦的使用框架。(感谢作者.jpg

两个小问题

在我们往下看之前,我们先提出两个小问题

一: request看起来只是一个静态的类实例,我们为什么可以直接使用 request.args 这样的表达式来获取当前 request 的 args 属性,而不用使用

1
2
3
4
5
6
7
from flask import Flask,get_current_request
app = Flask()
@app.route('/)
def index():
# 从当前request获取内容
request = get_current_request()
request.args

这种方式呢?flask 究竟是怎样将 request 映射到当前请求的呢

二: 在 flask 的生产环境中,常常是多个线程(协程)一起运行,request 这个类是怎么在这个环境下工作的呢?

要理解这两个问题,我们还是要从源码中来理解,还好 flask 以及它的底层依赖库的文档及代码注释都十分的详细。

Talk is cheap, Show me your code.

先找入口,永远是一个正确的决定

1
2
3
#file: flask/__init__.py
from .globals import current_app, g, request, session, _request_ctx_stack, \
_app_ctx_stack
1
2
3
4
5
6
7
8
9
10
11
12
#file: flask/globals.py
from functools import partial
from werkzeug.local import LocalStack, LocalProxy
def _lookup_req_object(name):
top = _request_ctx_stack.top
if top is None:
raise RuntimeError(_request_ctx_err_msg)
return getattr(top, name)
_request_ctx_stack = LocalStack()
request = LocalProxy(partial(_lookup_req_object, 'request'))

其中, partial 来自标准库,我们很容易就能查到它的文档。

这里你可以简单的把它理解为,它返回一个以它的第二个参数为参数的函数。

这里的 Partial 生成一个 callable 的 function,这个 function 主要是从 _request_ctx_stack 这个 LocalStack 对象获取堆栈顶部的第一个 RequestContext 对象,然后返回这个对象的 request 属性。

让我们继续深入, 这个 LocalProxy 又是个什么东西。

1
2
3
4
5
6
7
#file: werkzeug/local.py
class LocalProxy(object):
"""Acts as a proxy for a werkzeug local. Forwards all operations to
a proxied object. The only operations not supported for forwarding
are right handed operands and any kind of assignment.
... ...

看 doc 就能知道它是做什么的了,LocalProxy 主要是就一个 Proxy, 一个为 werkzeug 的 Local 对象服务的代理。他把所以作用到自己的操作全部“转发”到 它所代理的对象上去。

它是怎么实现这种代理的呢?这里我推荐一本书,《Python cookbook》我个人认为这本书涉及了 Python 中造轮子的方方面面。

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
# file: werkzeug/local.py
# 为了方便说明,我对源码做了少量修改与删除
class LocalProxy(object):
# slots, 减小内存占用
__slots__ = ('__local', '__dict__', '__name__', '__wrapped__')
def __init__(self, local, name=None):
# 这里的local被设置成前面两个下划线是python的一种约定,因为python中没有
# 真正的Private member, 程序猿以两个下划线代表私有变量,讲道理
# 在正常情况下,你不应该去访问私有变量。你可以把它看做self.__local = local
object.__setattr__(self, '_LocalProxy__local', local)
object.__setattr__(self, '__name__', name)
# 下面的与我们这次分析无关
if callable(local) and not hasattr(local, '__release_local__'):
# "local" is a callable that is not an instance of Local or
# LocalManager: mark it as a wrapped function.
object.__setattr__(self, '__wrapped__', local)
def _get_current_object(self):
"""返回当前对象,通常情况下你不该调用它,除非你由于性能原因需要这个隐藏在
proxy之后的对象,或者你需要将这个对象用到其他地方去
"""
# 这里主要是判断代理的对象是不是一个werkzeug的Local对象,在我们分析request
# 的过程中,不会用到这块逻辑。
if not hasattr(self.__local, '__release_local__'):
# 从我们上面分析得到的partial,它首先被传入__init__方法的local中,被设置成
# __local, 当我们调用self.__local()时,实际上调用了
# partial(_lookup_req_object, 'request')()
# 也就是调用了 _lookup_req_object('request')
# 也即 _request_ctx_stack.top.request
return self.__local()
try:
return getattr(self.__local, self.__name__)
except AttributeError:
raise RuntimeError('no object bound to %s' % self.__name__)
# 一堆的魔法方法,重载重载重载, 将(几乎?)所有的魔法方法全部重载, 全部代理到内部的
# request对象来
... ...
__setattr__ = lambda x, n, v: setattr(x._get_current_object(), n, v)
__delattr__ = lambda x, n: delattr(x._get_current_object(), n)
__str__ = lambda x: str(x._get_current_object())
__lt__ = lambda x, o: x._get_current_object() < o
__le__ = lambda x, o: x._get_current_object() <= o
__eq__ = lambda x, o: x._get_current_object() == o
__ne__ = lambda x, o: x._get_current_object() != o
__gt__ = lambda x, o: x._get_current_object() > o
__ge__ = lambda x, o: x._get_current_object() >= o
__cmp__ = lambda x, o: cmp(x._get_current_object(), o) # noqa
__hash__ = lambda x: hash(x._get_current_object())
... ...

到这里,我们已经能解释第一个问题了,为什么 request 可以使用request.args 来获取当前的请求中的内容。

LocalProxy 作为一个代理,通过自定义魔法方法。代理了我们对于 request 的所有操作, 使之指向到真正的 request 对象。( _request_ctx_stack.top.request 真正的对象)

那么第二个问题呢?request 怎么保证在多线程环境下依旧能够正常的工作呢?

还是回到 globals.py 吧

1
2
3
4
5
6
7
8
9
10
11
12
#file: flask/globals.py
from functools import partial
from werkzeug.local import LocalStack, LocalProxy
def _lookup_req_object(name):
top = _request_ctx_stack.top
if top is None:
raise RuntimeError(_request_ctx_err_msg)
return getattr(top, name)
_request_ctx_stack = LocalStack()
request = LocalProxy(partial(_lookup_req_object, 'request'))

这里重点就是_request_ctx_stack变量了。

让我们来看看 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# file: werkzeug/local.py
class LocalStack(object):
"""This class works similar to a :class:`Local` but keeps a stack
of objects instead. This is best explained with an example::
>>> ls = LocalStack()
>>> ls.push(42)
>>> ls.top
42
>>> ls.push(23)
>>> ls.top
23
>>> ls.pop()
23
>>> ls.top
42
"""
def __init__(self):
# 其实LocalStack主要还是用到了另外一个Local类
# 它的一些关键的方法也被代理到了这个Local类上
# 相对于Local类来说,它多实现了一些和堆栈“Stack”相关方法,比如push、pop之类
# 所以,我们只要直接看Local代码就可以
self._local = Local()
def push(self, obj):
"""Pushes a new item to the stack"""
rv = getattr(self._local, 'stack', None)
if rv is None:
self._local.stack = rv = []
rv.append(obj)
return rv
def pop(self):
"""Removes the topmost item from the stack, will return the
old value or `None` if the stack was already empty.
"""
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()
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
# file: werkzeug/local.py
# 因为每一个线程都拥有自己的协程所以我们可以只用协程的id号来区分context,
# 如果协程不可用那我们就使用当前线程的id即可。
# 总之,这个get_ident方法将会返回当前的协程/线程ID,这对于每一个请求都是唯一的
try:
from greenlet import getcurrent as get_ident
except ImportError:
try:
from thread import get_ident
except ImportError:
from _thread import get_ident
class Local(object):
__slots__ = ('__storage__', '__ident_func__')
def __init__(self):
object.__setattr__(self, '__storage__', {})
object.__setattr__(self, '__ident_func__', get_ident)
def __setattr__(self, name, value):
# 设置值
# 以不同的线程(协程)id作为字典的键
# 从而获得隔离的效果
ident = self.__ident_func__()
storage = self.__storage__
try:
storage[ident][name] = value
except KeyError:
storage[ident] = {name: value}
def __getattr__(self, name):
# 获取值
# 以不同的线程(协程)id作为字典的键
# 从而获得隔离的效果
try:
return self.__storage__[self.__ident_func__()][name]
except KeyError:
raise AttributeError(name)
def __delattr__(self, name):
# 删除值
# 以不同的线程(协程)id作为字典的键
# 从而获得隔离的效果
try:
del self.__storage__[self.__ident_func__()][name]
except KeyError:
raise AttributeError(name)
# 重载了这三个魔法方法之后
# Local().some_value 不再是它看上去那么简单了:
# 首先我们先调用get_ident方法来获取当前运行的线程/协程ID
# 然后获取这个ID空间下的some_value属性,就像这样:
#
# Local().some_value 等价于 Local()[current_thread_id()].some_value
#
# 设置属性的时候也是这个道理

到这里,我们也解释了第二个问题了,通过使用了当前的线程/协程 ID,加上重载一些魔法 方法,隔离了不同的 stack,Flask 实现了让不同工作线程都使用了自己的那一份 stack 对象。这样保证了 request 的正常工作。

The more

做过多线程编程的同学可能对 ThreadLocal 有一定了解, 那么为什么 Flask 不使用标准库里的 ThreadLocal, 而要自己实现一个 Local 呢(werkzeug 与 Flask 是一个作者)?

其实主要是性能考虑,因为 Python 中有协程这玩意的存在,所以不能用 ThreadLocal,否则就不能利用到 Python 的协程优势了,关于协程,在我的另一篇 blog 中有一点点小小的理解。