Flask 大白话

flask到底是怎么运行的?不阅读源代码的前提下,记录他人解析源代码的笔记如下。我总有一天要亲自阅读源代码,并且不限于flask。

代码解析系列地址:flask 源码解析:简介 | Cizixs Write Here

如果有疑问,联系我删除本文。

启动

如下是hello world的示例:

1
2
3
4
5
6
7
8
9
from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
return 'Hello, World!'

if __name__ == '__main__':
app.run()

那么,核心对象是app。启动的run方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def run(self, host=None, port=None, debug=None, **options):
"""Runs the application on a local development server."""
from werkzeug.serving import run_simple

# 如果host 和 port 没有指定,设置 host 和 port 的默认值 127.0.0.1 和 5000
if host is None:
host = '127.0.0.1'
if port is None:
server_name = self.config['SERVER_NAME']
if server_name and ':' in server_name:
port = int(server_name.rsplit(':', 1)[1])
else:
port = 5000

# 调用 werkzeug.serving 模块的 run_simple 函数,传入收到的参数
# 注意第三个参数传进去的是 self,也就是要执行的 web application
try:
run_simple(host, port, self, **options)
finally:
self._got_first_request = False

关键之一:run_simple方法,从这里可以转到flask中导入的关键库werkzeug。
监听在指定的端口,收到 HTTP 请求的时候解析为 WSGI 格式,然后调用 app 去执行处理的逻辑。对应的执行逻辑在 werkzeug.serving:WSGIRequestHandler 的 run_wsgi 中有这么一段代码:

1
2
3
4
5
6
7
8
9
10
11
def execute(app):
application_iter = app(environ, start_response)
try:
for data in application_iter:
write(data)
if not headers_sent:
write(b'')
finally:
if hasattr(application_iter, 'close'):
application_iter.close()
application_iter = None

这里,这个execute函数中的参数app,就是用的flask核心对象app。application_iter = app(environ, start_response) 就是调用代码获取结果的地方。

flask中的核心对象app类,实现了call方法,进入这里:

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
def __call__(self, environ, start_response):
"""Shortcut for :attr:`wsgi_app`."""
return self.wsgi_app(environ, start_response)

def wsgi_app(self, environ, start_response):
"""The actual WSGI application.
"""
# 创建请求上下文,并把它压栈。这个在后面会详细解释
ctx = self.request_context(environ)
ctx.push()
error = None

try:
try:
# 正确的请求处理路径,会通过路由找到对应的处理函数
response = self.full_dispatch_request()
except Exception as e:
# 错误处理,默认是 InternalServerError 错误处理函数,客户端会看到服务器 500 异常
error = e
response = self.handle_exception(e)
return response(environ, start_response)
finally:
if self.should_ignore_error(error):
error = None
# 不管处理是否发生异常,都需要把栈中的请求 pop 出来
ctx.auto_pop(error)

关于线程隔离的栈处理,每次请求进来都会入栈,结束就出栈。

关键在于full_dispatch_request方法,根据路由获取对应的视图函数。此函数代码关键:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def full_dispatch_request(self):
"""Dispatches the request and on top of that performs request
pre and postprocessing as well as HTTP exception catching and
error handling.
"""
self.try_trigger_before_first_request_functions()
try:
request_started.send(self)
rv = self.preprocess_request()
if rv is None:
rv = self.dispatch_request()
except Exception as e:
rv = self.handle_user_exception(e)
return self.finalize_request(rv)

NOTE:self.dispatch_request() 返回的是处理函数的返回结果(比如 hello world 例子中返回的字符串),finalize_request 会把它转换成 Response 对象。

在dispatch_request 之前我们看到 preprocess_request,之后看到 finalize_request,它们里面包括了请求处理之前和处理之后的很多 hooks 。这些 hooks 包括:

  • 第一次请求处理之前的 hook 函数,通过 before_first_request 定义
  • 每个请求处理之前的 hook 函数,通过 before_request 定义
  • 每个请求正常处理之后的 hook 函数,通过 after_request 定义
  • 不管请求是否异常都要执行的 teardown_request hook 函数

路由的过程

针对每一个url,查找不同的处理函数。
在执行查找之前,需要有一个规则列表,它存储了 url 和处理函数的对应关系。

在flask中,路由的实现,使用了一个装饰器:就像在hello world代码实例中一样

1
@app.route('/')

app的route方法,参数是url路径。

另外可以通过 app.add_url_rule,这个方法的签名为 add_url_rule(self, rule, endpoint=None, view_func=None, ** options),参数的含义如下:

  • rule: url 规则字符串,可以是静态的 /path,也可以包含 /
  • endpoint:要注册规则的 endpoint,默认是 view_func 的名字
  • view_func:对应 url 的处理函数,也被称为视图函数
1
2
3
4
def hello():
return "hello, world!"

app.add_url_rule('/', 'hello', hello)

接下来看这个关键的route方法:

1
2
3
4
5
6
7
8
9
10
11
12
def route(self, rule, **options):
"""A decorator that is used to register a view function for a
given URL rule. This does the same thing as :meth:`add_url_rule`
but is intended for decorator usage.
"""

def decorator(f):
endpoint = options.pop('endpoint', None)
self.add_url_rule(rule, endpoint, f, **options)
return f

return decorator

装饰器实现的功能,依然是调用了add_url_rule这个函数,而这个方法,详细代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
"""Connects a URL rule. Works exactly like the :meth:`route`
decorator. If a view_func is provided it will be registered with the
endpoint.
"""

methods = options.pop('methods', None)

rule = self.url_rule_class(rule, methods=methods, **options)
self.url_map.add(rule)
if view_func is not None:
old_func = self.view_functions.get(endpoint)
if old_func is not None and old_func != view_func:
raise AssertionError('View function mapping is overwriting an '
'existing endpoint function: %s' % endpoint)
self.view_functions[endpoint] = view_func

可以看到它主要做的事情就是更新 self.url_map 和 self.view_functions 两个变量。找到变量的定义,发现 url_map 是 werkzeug.routeing:Map 类的对象,rule 是 werkzeug.routing:Rule 类的对象,view_functions 就是一个字典。这和我们之前预想的并不一样,这里增加了 Rule 和 Map 的封装,还把 url 和 view_func 保存到了不同的地方。

需要注意的是:每个视图函数的 endpoint 必须是不同的,否则会报 AssertionError。

重点在于:url - endpoint - view-function 三者

flask路由核心功能还是在werkzeug这里,它提供的路由功能,看如下实例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>> m = Map([
... Rule('/', endpoint='index'),
... Rule('/downloads/', endpoint='downloads/index'),
... Rule('/downloads/<int:id>', endpoint='downloads/show')
... ])
>>> urls = m.bind("example.com", "/")
>>> urls.match("/", "GET")
('index', {})
>>> urls.match("/downloads/42")
('downloads/show', {'id': 42})

>>> urls.match("/downloads")
Traceback (most recent call last):
...
RequestRedirect: http://example.com/downloads/
>>> urls.match("/missing")
Traceback (most recent call last):
...
NotFound: 404 Not Found

关键在于endpoint和url的映射,至于 endpoint 和 view function之间的匹配关系,werkzeug 是不管的,而上面也看到 flask 是把这个存放到字典中的。

回头看 dispatch_request,继续探寻路由匹配的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def dispatch_request(self):
"""Does the request dispatching. Matches the URL and returns the
return value of the view or error handler. This does not have to
be a response object. In order to convert the return value to a
proper response object, call :func:`make_response`.
"""

req = _request_ctx_stack.top.request
if req.routing_exception is not None:
self.raise_routing_exception(req)
rule = req.url_rule

# dispatch to the handler for that endpoint
return self.view_functions[rule.endpoint](** req.view_args)

这个方法做的事情就是找到请求对象 request,获取它的 endpoint,然后从
view_functions 找到对应 endpoint 的 view_func
,把请求参数传递过去,进行处理并返回。

而每个请求在进入的时候,多被压入了栈中,因此使用的时候只需要获取当前栈顶的内容即可得到url rule,

核心对象存在很多的视图函数,但是在每一个请求中,可以获取请求对象的endpoint,从而在views_functions中找到对应的vies_func._request_ctx_stack.top.request 保存着当前请求的信息,在每次请求过来的时候,flask 会把当前请求的信息保存进去,这样我们就能在整个请求处理过程中使用它。_request_ctx_stack 中保存的是 RequestContext 对象,它出现在 flask/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
class RequestContext(object):
def __init__(self, app, environ, request=None):
self.app = app
self.request = request
self.url_adapter = app.create_url_adapter(self.request)
self.match_request()


def match_request(self):
"""Can be overridden by a subclass to hook into the matching
of the request.
"""
try:
url_rule, self.request.view_args = \
self.url_adapter.match(return_rule=True)
self.request.url_rule = url_rule
except HTTPException as e:
self.request.routing_exception = e


class Flask(_PackageBoundObject):
def create_url_adapter(self, request):
"""Creates a URL adapter for the given request. The URL adapter
is created at a point where the request context is not yet set up
so the request is passed explicitly.
"""
if request is not None:
return self.url_map.bind_to_environ(request.environ,
server_name=self.config['SERVER_NAME'])
  • 通过 @app.route或者app.add_url_rule 注册应用 url 对应的处理函数
  • 每次请求过来的时候,会事先调用路由匹配的逻辑,把路由结果保存起来
  • dispatch_request 根据保存的路由结果,调用对应的视图函数

上下文

上下文分为应用上下文和请求上下文。

每一段程序都有很多外部变量。只有像Add这种简单的函数才是没有外部变量的。一旦你的一段程序有了外部变量,这段程序就不完整,不能独立运行。你为了使他们运行,就要给所有的外部变量一个一个写一些值进去。这些值的集合就叫上下文。 – vzch

核心代码如下:

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
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 = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))
g = LocalProxy(partial(_lookup_app_object, 'g'))

LocalStack类是一个实现了线程隔离的类。
比如,在 flask 中,视图函数需要知道它执行情况的请求信息(请求的 url,参数,方法等)以及应用信息(应用中初始化的数据库等),才能够正确运行。

最直观地做法是把这些信息封装成一个对象,作为参数传递给视图函数。但是这样的话,所有的视图函数都需要添加对应的参数,即使该函数内部并没有使用到它。

flask 的做法是把这些信息作为类似全局变量的东西,视图函数需要的时候,可以使用 from flask import request 获取。但是这些对象和全局变量不同的是——它们必须是动态的,因为在多线程或者多协程的情况下,每个线程或者协程获取的都是自己独特的对象,不会互相干扰。

这里的实现用到了两个东西:LocalStack 和 LocalProxy。它们两个的结果就是我们可以动态地获取两个上下文的内容,在并发程序中每个视图函数都会看到属于自己的上下文,而不会出现混乱。

LocalStack 和 LocalProxy 都是 werkzeug 提供的,定义在 local.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
# since each thread has its own greenlet we can just use those as identifiers
# for the context. If greenlets are not available we fall back to the
# current thread ident depending on where it is.
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):
# 数据保存在 __storage__ 中,后续访问都是对该属性的操作
object.__setattr__(self, '__storage__', {})
object.__setattr__(self, '__ident_func__', get_ident)

def __call__(self, proxy):
"""Create a proxy for a name."""
return LocalProxy(self, proxy)

# 清空当前线程/协程保存的所有数据
def __release_local__(self):
self.__storage__.pop(self.__ident_func__(), None)

# 下面三个方法实现了属性的访问、设置和删除。
# 注意到,内部都调用 `self.__ident_func__` 获取当前线程或者协程的 id,然后再访问对应的内部字典。
# 如果访问或者删除的属性不存在,会抛出 AttributeError。
# 这样,外部用户看到的就是它在访问实例的属性,完全不知道字典或者多线程/协程切换的实现
def __getattr__(self, name):
try:
return self.__storage__[self.__ident_func__()][name]
except KeyError:
raise AttributeError(name)

def __setattr__(self, name, value):
ident = self.__ident_func__()
storage = self.__storage__
try:
storage[ident][name] = value
except KeyError:
storage[ident] = {name: value}

def __delattr__(self, name):
try:
del self.__storage__[self.__ident_func__()][name]
except KeyError:
raise AttributeError(name)

可以看到,Local 对象内部的数据都是保存在 storage 属性的,这个属性变量是个嵌套的字典:map[ident]map[key]value。

最外面字典key是线程或者协程的 identity,value 是另外一个字典,这个内部字典就是用户自定义的 key-value 键值对。用户访问实例的属性,就变成了访问内部的字典,外面字典的 key 是自动关联的。__ident_func 是 协程的 get_current 或者线程的 get_ident,从而获取当前代码所在线程或者协程
的id。_request_ctx_stack是多线程或者协程隔离的栈结构,request 每次都会调用_lookup_req_object 栈头部的数据来获取保存在里面的 requst context。在这个栈这里,请求上下文在app部分被入栈,LocalStack也实现了栈的三个方法。

1
2
3
4
5
6
ctx = self.request_context(environ)
ctx.push()

# 这个方法
def request_context(self, environ):
return RequestContext(self, environ)

它调用了 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
class RequestContext(object):
"""The request context contains all request relevant information. It is
created at the beginning of the request and pushed to the
`_request_ctx_stack` and removed at the end of it. It will create the
URL adapter and request object for the WSGI environment provided.
"""

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.match_request()

def match_request(self):
"""Can be overridden by a subclass to hook into the matching
of the request.
"""
try:
url_rule, self.request.view_args = \
self.url_adapter.match(return_rule=True)
self.request.url_rule = url_rule
except HTTPException as e:
self.request.routing_exception = e

def push(self):
"""Binds the request context to the current context."""
# 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)

self.session = self.app.open_session(self.request)
if self.session is None:
self.session = self.app.make_null_session()

def pop(self, exc=_sentinel):
"""Pops the request context and unbinds it by doing that. This will
also trigger the execution of functions registered by the
:meth:`~flask.Flask.teardown_request` decorator.
"""
app_ctx = self._implicit_app_ctx_stack.pop()

try:
clear_request = False
if not self._implicit_app_ctx_stack:
self.app.do_teardown_request(exc)

request_close = getattr(self.request, 'close', None)
if request_close is not None:
request_close()
clear_request = True
finally:
rv = _request_ctx_stack.pop()

# get rid of circular dependencies at the end of the request
# so that we don't require the GC to be active.
if clear_request:
rv.request.environ['werkzeug.request'] = None

# Get rid of the app as well if necessary.
if app_ctx is not None:
app_ctx.pop(exc)

def auto_pop(self, exc):
if self.request.environ.get('flask._preserve_context') or \
(exc is not None and self.app.preserve_context_on_exception):
self.preserved = True
self._preserved_exc = exc
else:
self.pop(exc)

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

def __exit__(self, exc_type, exc_value, tb):
self.auto_pop(exc_value)

每个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。

请求和响应

对于 TCP 层来说,请求就是传输的数据(二进制的数据流),它只要发送给对应的应用程序就行了;
对于 HTTP 层的服务器来说,请求必须是符合 HTTP 协议的内容;
对于 WSGI server 来说,请求又变成了文件流,它要读取其中的内容,把 HTTP 请求包含的各种信息保存到一个字典中,调用 WSGI app;
对于 flask app 来说,请求就是一个对象,当需要某些信息的时候,只需要读取该对象的属性或者方法就行了。

1
2
3
4
from flask import request

with app.request_context(environ):
assert request.method == 'POST'

需要用request的时候直接导入即可,并且请求也是线程隔离的,并且封装了Request对象。它接受
WSGI server 传递过来的 environ,并且,这玩意不能改变。

请求对象增加了一些属性,这些属性和 flask 的逻辑有关,比如 view_args、blueprint、json 处理等。

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
from werkzeug.wrappers import Request as RequestBase


class Request(RequestBase):
"""
The request object is a :class:`~werkzeug.wrappers.Request` subclass and
provides all of the attributes Werkzeug defines plus a few Flask
specific ones.
"""

#: The internal URL rule that matched the request. This can be
#: useful to inspect which methods are allowed for the URL from
#: a before/after handler (``request.url_rule.methods``) etc.
url_rule = None

#: A dict of view arguments that matched the request. If an exception
#: happened when matching, this will be ``None``.
view_args = None

@property
def max_content_length(self):
"""Read-only view of the ``MAX_CONTENT_LENGTH`` config key."""
ctx = _request_ctx_stack.top
if ctx is not None:
return ctx.app.config['MAX_CONTENT_LENGTH']

@property
def endpoint(self):
"""The endpoint that matched the request. This in combination with
:attr:`view_args` can be used to reconstruct the same or a
modified URL. If an exception happened when matching, this will
be ``None``.
"""
if self.url_rule is not None:
return self.url_rule.endpoint

@property
def blueprint(self):
"""The name of the current blueprint"""
if self.url_rule and '.' in self.url_rule.endpoint:
return self.url_rule.endpoint.rsplit('.', 1)[0]

@property
def is_json(self):
mt = self.mimetype
if mt == 'application/json':
return True
if mt.startswith('application/') and mt.endswith('+json'):
return True
return False

之后如何如何,我在这里记笔记感觉毫无意义。这不是我所写出来的东西,也就是说就算我理解了,也不是我的产出。
FUCK - 滚去写项目了,再看源代码。