系列文章先暂时停更一下。今天换换口味。
久闻 Flask 是众多 Pythonist 喜欢的框架。这次借着换工作的机会熟悉一下 Flask
将解读 Flask 的源码放在一篇文章里,势必会造成广度有余而深度不足。所以本想定位于 Flask 源码初步解读。
之前在 https://www.zhihu.com/question/28509408/answer/299763091 分享过自己一点阅读代码的粗浅的经验,是以阅读一个 Django 的应用为案例的。这里借着读 Flask 本身分享一下我的看法。
读源码,是一个技术活。一是忌讳要想读懂全部,另一个忌讳是以为自己能一下子毫无障碍的读懂全部代码。
好,坐好,预备,开车。
Flask 是一个微 Web 框架,换而言之,代码量少的 Web 框架。当然,其实 Flask 框架是一个微框架,但『常规的 Flask 应用』本身的代码加起来一点都不比『Django 应用』少。这个地方我们后面会讲到。
在阅读 Flask 相关代码的之前,先头脑风暴一下:
如何设计一个 Web 框架?
当心中对这个问题有一定的了解之后,读 Flask 代码会更好。
首先,Web 框架是为了提升 Web 开发的。(XX 框架是为了提升 XX 开发的), 这种提升可能会是 开发体验 / 性能。
我们来看看那个 Python 世界最基础的 wsgi app 相关代码。
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/plain')])
return ['Hello World!']
在之前的文章,我也借 Django 的 DRF 提到过这个极简的代码。
但这个简单的 webapp,显然是啥玩意都不够用的。比如说:
def application(environ, start_response):
# 直接 thread local 支持多个请求。
# 依据 environ 判断路由
# 依据 路由 执行相关 view 层方法
# 在相关 view 层方法内执行相关逻辑
start_response('200 OK', [('Content-Type', 'text/plain')])
# 返回对应响应
return response
当然,思路是这么个思路,这个思路也确实非常的命令式,非常的面向过程。
至于我们如何把这个面向过程的思路变成面向对象的设计与实现,则需要更加细致的思考这些问题。
利用 Flask 作者的另一个库 werkzeug 的案例中有这么一个东西。
https://github.com/pallets/werkzeug/blob/master/examples/shortly/shortly.py
几百行代码就不贴在这里了。仔细看看还是挺有趣的。 这个可以算作另一个超简的 Webapp 了。
Flask 算作在这个基础上进行一定的扩展而成。
看完上面这个就可以出去吹牛逼可以自己写一个极简 Web 框架了。
那么,你可能有疑问,为何有了 Flask 之后,是否需要看这个更底层的 Werkzeug 的库,当然,有必要咯,Python 世界除了老牌的比较流行的 Django/Flask, 还有一个新星,叫做 APIStar
https://github.com/encode/apistar
我们就拿这个 flask 的极简案例,进行首次阅读 Flask 代码。
# hello.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return 'Hello, World!'
$ FLASK_APP=hello.py flask run
从请求到响应的整个流程,Flask 的是怎么处理请求的?
首先 flask run 之后,发生了什么?
先初始化环境变量,然后导入 dotenv 文件,然后执行 run_command 方法,找到 hello.py 然后导入
#cli.py#run_command 方法
app = DispatchingApp(info.load_app, use_eager_loading=eager_loading)
# 上一行代表着其实我们每次在本地 flask run 的时候,起的服务并不是 flask_app, 而是被 DispatchingApp 包装了一层的 flask app
from werkzeug.serving import run_simple
run_simple(host, port, app, use_reloader=reload, use_debugger=debugger,
threaded=with_threads, ssl_context=cert)
进行这层包装之后,就可以显示 WERKZEUG 的所谓在浏览器中的 报错信息了。
通常开发时这里的 run_simple 最后会调用 run_with_reloader , 每当程序退出的时候,reloader 就依照策略重新跑一次 reload 一次。
def run_with_reloader(main_func, extra_files=None, interval=1,
reloader_type='auto'):
"""Run the given function in an independent python interpreter."""
import signal
reloader = reloader_loops[reloader_type](extra_files, interval)
signal.signal(signal.SIGTERM, lambda *args: sys.exit(0))
try:
if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
t = threading.Thread(target=main_func, args=())
t.setDaemon(True)
t.start()
reloader.run()
else:
sys.exit(reloader.restart_with_reloader())
except KeyboardInterrupt:
pass
好,服务起来了。
我们先看 Flask 类里面的比较关键的两个方法:
class Flask(_PackageBoundObject):
# 一些方法 ......
def full_dispatch_request(self):
# 主要是执行一些方法,最后返回响应
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)
# ??? TODO
return self.finalize_request(rv)
# 这里是我们熟悉的 environ, 和 start_response
def wsgi_app(self, environ, start_response):
"""
:param environ: a WSGI environment
:param start_response: a callable accepting a status code,
a list of headers and an optional
exception context to start the response
"""
# 在这里对 environ 进行封装,创建请求上下文
ctx = self.request_context(environ)
error = None
try:
try:
# 这里将请求上下文压入 _request_ctx_stack
ctx.push()
response = self.full_dispatch_request()
except Exception as e:
error = e
response = self.handle_exception(e)
except:
error = sys.exc_info()[1]
raise
return response(environ, start_response)
finally:
if self.should_ignore_error(error):
error = None
# 这里将创建的请求上下文从中 _request_ctx_stack pop 出来
ctx.auto_pop(error)
从 wsgi_app 泪看,就可以看到我们之前在当时在开脑洞时候看到的。
话说回来?
这个 ctx 是啥?
当然,flask 不带 ORM, 这我们也就不研究了。
– TODO: 在这里需要重构一下
不过话说回来 请求上下文的容器 request_ctx_stack 到底是啥?
另一种本地数据存储方式。
在多线程的情况下,每一个请求都会创建一个线程,从这个请求被发起到销毁,我想拥有单独的变量(修改这个变量不会影响到其他变量),比如 sessions 之类。
显然,在多线程的情况下,以上的需求完全可以通过 threadlocal 来实现。
翻了 werkzeug 的文档,找到了原因:
因为 python 里面的并发模型并不只有多线程一种。比如 greenlets, 每一个请求,都在一个线程里面。
在 Django 完成一个 View 层的逻辑是这样的,Django 封装好了请求,请求经过 middleware 的处理,最后调用 login 函数,并且传入 request 方便 view 函数进行处理。
def login(request):
if request.method == 'POST':
error = someerror
return render_template('login.html', error=error)
在 Flask 完成一个 View 层的逻辑是这样的
from flask import request
@app.route('/login', methods=['POST', 'GET'])
def login():
if request.method == 'POST':
error = someerror
return render_template('login.html', error=error)
假如我是一个爱问问题的年轻人,这里肯定会有疑惑:
从外部 import 过来,那就是利用了 python 自带的 import 单例模式。 那么线程和线程之间拿到的肯定是同一个 request 呀。但 Django 里面每个 request 都是不一样的,否则一些很基础功能的比如已经认证的用户就无法拿到了。
我已经不是那个爱问问题的年轻人,因为年纪已经不小了。逃…
显然,每一次在 view 层引用的 request 肯定不是同一个 request , 那么,这是如何做到的呢?比如用 ThreadLocal , ThreadLocal 通过每个线程不同的 ID 拿到的本地变量,于是我们查看一下对应的实现。 这个 request 来自于 global.py , 使用了一个 werkzeug.local 里面的 LocalProxy
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)
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)
# 这就是我们需要的注意的地方,LocalProxy
request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))
g = LocalProxy(partial(_lookup_app_object, 'g'))
看到这里一阵蛋疼,貌似没有 threadlocal ?再次查看相关实现最后还是定位到了如何区分不同的 request 的核心代码。
# 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
# greenlet 的代码是 C, 时间长没看 C 代码了,看了半天没看明白
# 翻了文档返回当前的 greenlet, 也就是返回调用此函数的 greenlet
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__', {})
# 这里传递的 ident 就可以直接
object.__setattr__(self, '__ident_func__', get_ident)
class LocalStack:
# 用 local 实现的栈
class LocalProxy:
# 一个 local 的代理器
def __getattr__(self, name):
if name == '__members__':
return dir(self._get_current_object())
return getattr(self._get_current_object(), name)
即:
接下来回头看一下处理 request 的逻辑
from flask import request
@app.route('/login', methods=['POST', 'GET'])
def login():
# 这个 request 哪里来?
if request.method == 'POST':
error = someerror
return render_template('login.html', error=error)
于是,我们就知道了,当引用 request 这个 LocalProxy 的时候,引用的确实是同一个名称为 request 变量,并且这个变量也确实是 LocalProxy 的实例
但是当使用 request.method 的时候,LocalProxy 重载了 取到的则是另一个『请求对象』的 method.
于是拿到当前请求的信息。
当然,其实我们也可以依据利用这个技巧写一个 currentuser 的 ProxyLocal, 然后在每个 view 层里面使用 user.has_something 进行操作。
ChangeLog: