本文章粘贴自 https://blog.tonyseek.com/post/the-context-mechanism-of-flask/数据库
用过 Flask 作 Web 开发的同窗应该不会不记得 App Context 和 Request Context 这两个名字——这两个 Context 算是 Flask 中比较特点的设计。[1]flask
从一个 Flask App 读入配置并启动开始,就进入了 App Context,在其中咱们能够访问配置文件、打开资源文件、经过路由规则反向构造 URL。[2] 当一个请求进入开始被处理时,就进入了 Request Context,在其中咱们能够访问请求携带的信息,好比 HTTP Method、表单域等。[3]网络
因此,这两个 Context 也成了 Flask 框架复杂度比较集中的地方,对此有评价认为 Flask 的这种设计比 Django、Tornado 等框架的设计更为晦涩。[4] 我不认同这种评价。对于一个 Web 应用来讲,“应用” 和 “请求” 的两级上下文在理念上是现实存在的,若是理解了它们,那么使用 Flask 并不会晦涩;即便是使用 Django、Tornado,理解了它们的 Context 也很是有利于作比官网例子更多的事情(例如编写 Middleware)。数据结构
我由于开发 Flask 扩展,对这两个 Context 的具体实现也研究了一番,同时还解决了一些本身以前“知道结论不知道过程”的疑惑,因此撰写本文记录下来。多线程
从面向对象设计的角度看,对象是保存“状态”的地方。Python 也是如此,一个对象的状态都被保存在对象携带的一个特殊字典中,能够经过 vars 函数拿到它。app
Thread Local 则是一种特殊的对象,它的“状态”对线程隔离 —— 也就是说每一个线程对一个 Thread Local 对象的修改都不会影响其余线程。这种对象的实现原理也很是简单,只要以线程的 ID 来保存多份状态字典便可,就像按照门牌号隔开的一格一格的信箱。框架
在 Python 中得到一个这样的 Thread Local 最简单的方法是 threading.local():异步
>>> import threading >>> storage = threading.local() >>> storage.foo = 1 >>> print(storage.foo) 1 >>> class AnotherThread(threading.Thread): ... def run(self): ... storage.foo = 2 ... print(storage.foo) # 这这个线程里已经修改了 >>> >>> another = AnotherThread() >>> another.start() 2 >>> print(storage.foo) # 可是在主线程里并无修改 1
Werkzeug 没有直接使用 threading.local,而是本身实现了 werkzeug.local.Local 类。后者和前者有一些区别:ide
除 Local 外,Werkzeug 还实现了两种数据结构:LocalStack 和 LocalProxy。函数
LocalStack 是用 Local 实现的栈结构,能够将对象推入、弹出,也能够快速拿到栈顶对象。固然,全部的修改都只在本线程可见。和 Local 同样,LocalStack 也一样实现了支持 release_pool 的接口。
LocalProxy 则是一个典型的代理模式实现,它在构造时接受一个 callable 的参数(好比一个函数),这个参数被调用后的返回值自己应该是一个 Thread Local 对象。对一个 LocalProxy 对象的全部操做,包括属性访问、方法调用(固然方法调用就是属性访问)甚至是二元操做 [6] 都会转发到那个 callable 参数返回的 Thread Local 对象上。
LocalProxy 的一个使用场景是 LocalStack 的 __call__ 方法。好比 my_local_stack 是一个 LocalStack 实例,那么 my_local_stack() 能返回一个 LocalProxy 对象,这个对象始终指向 my_local_stack 的栈顶元素。若是栈顶元素不存在,访问这个 LocalProxy 的时候会抛出 RuntimeError。
Flask 是一个基于 Werkzeug 实现的框架,因此 Flask 的 App Context 和 Request Context 也理所固然地基于 Werkzeug 的 Local Stack 实现。
在概念上,App Context 表明了“应用级别的上下文”,好比配置文件中的数据库链接信息;Request Context 表明了“请求级别的上下文”,好比当前访问的 URL。
这两种上下文对象的类定义在 flask.ctx 中,它们的用法是推入 flask.globals 中建立的 _app_ctx_stack 和 _request_ctx_stack 这两个单例 Local Stack 中。由于 Local Stack 的状态是线程隔离的,而 Web 应用中每一个线程(或 Greenlet)同时只处理一个请求,因此 App Context 对象和 Request Context 对象也是请求间隔离的。
当 app = Flask(__name__) 构造出一个 Flask App 时,App Context 并不会被自动推入 Stack 中。因此此时 Local Stack 的栈顶是空的,current_app 也是 unbound 状态。
>>> from flask import Flask >>> from flask.globals import _app_ctx_stack, _request_ctx_stack >>> >>> app = Flask(__name__) >>> _app_ctx_stack.top >>> _request_ctx_stack.top >>> _app_ctx_stack() <LocalProxy unbound> >>> >>> from flask import current_app >>> current_app <LocalProxy unbound>
这也是一些 Flask 用户可能被坑的地方 —— 好比编写一个离线脚本时,若是直接在一个 Flask-SQLAlchemy 写成的 Model 上调用 User.query.get(user_id),就会遇到 RuntimeError。由于此时 App Context 还没被推入栈中,而 Flask-SQLAlchemy 须要数据库链接信息时就会去取 current_app.config,current_app 指向的倒是 _app_ctx_stack 为空的栈顶。
解决的办法是运行脚本正文以前,先将 App 的 App Context 推入栈中,栈顶不为空后 current_app 这个 Local Proxy 对象就天然能将“取 config 属性” 的动做转发到当前 App 上了:
>>> ctx = app.app_context() >>> ctx.push() >>> _app_ctx_stack.top <flask.ctx.AppContext object at 0x102eac7d0> >>> _app_ctx_stack.top is ctx True >>> current_app <Flask '__main__'> >>> >>> ctx.pop() >>> _app_ctx_stack.top >>> current_app <LocalProxy unbound>
那么为何在应用运行时不须要手动 app_context().push() 呢?由于 Flask App 在做为 WSGI Application 运行时,会在每一个请求进入的时候将请求上下文推入 _request_ctx_stack 中,而请求上下文必定是 App 上下文之中,因此推入部分的逻辑有这样一条:若是发现 _app_ctx_stack为空,则隐式地推入一个 App 上下文。
因此,请求中是不须要手动推上下文入栈的,可是离线脚本须要手动推入 App Context。若是没有什么特殊困难,我更建议用 Flask-Script 来写离线任务。[7]
到此为止,就出现两个疑问:
我最初也被这两个疑问困惑过。后来看了一些资料,就明白了 Flask 为什么要设计成这样。这两个作法给予咱们 多个 Flask App 共存 和 非 Web Runtime 中灵活控制 Context 的可能性。
咱们知道对一个 Flask App 调用 app.run() 以后,进程就进入阻塞模式并开始监听请求。此时是不可能再让另外一个 Flask App 在主线程运行起来的。那么还有哪些场景须要多个 Flask App 共存呢?前面提到了,一个 Flask App 实例就是一个 WSGI Application,那么 WSGI Middleware 是容许使用组合模式的,好比:
from werkzeug.wsgi import DispatcherMiddleware from biubiu.app import create_app from biubiu.admin.app import create_app as create_admin_app application = DispatcherMiddleware(create_app(), { '/admin': create_admin_app() })
这个例子就利用 Werkzeug 内置的 Middleware 将两个 Flask App 组合成一个一个 WSGI Application。这种状况下两个 App 都同时在运行,只是根据 URL 的不一样而将请求分发到不一样的 App 上处理。
Note
须要注意的是,这种用法和 Flask 的 Blueprint 是有区别的。Blueprint 虽然和这种用法很相似,但前者本身没有 App Context,只是同一个 Flask App 内部整理资源的一种方式,因此多个 Blueprint 可能共享了同一个 Flask App;后者面向的是全部 WSGI Application,而不只仅是 Flask App,即便是把一个 Django App 和一个 Flask App 用这种用法整合起来也是可行的。
若是仅仅在 Web Runtime 中,多个 Flask App 同时工做倒不是问题。毕竟每一个请求被处理的时候是身处不一样的 Thread Local 中的。可是 Flask App 不必定仅仅在 Web Runtime 中被使用 —— 有两个典型的场景是在非 Web 环境须要访问上下文代码的,一个是离线脚本(前面提到过),另外一个是测试。这两个场景即所谓的“Running code outside of a request”。
离线脚本或者测试这类非 Web 环境和和 Web 环境不一样 —— 前者通常只在主线程运行。
设想,一个离线脚本须要操做两个 Flask App 关联的上下文,应该怎么办呢?这时候栈结构的 App Context 优点就发挥出来了。
from biubiu.app import create_app from biubiu.admin.app import create_app as create_admin_app app = create_app() admin_app = create_admin_app() def copy_data(): with app.app_context(): data = read_data() # fake function for demo with admin_app.app_context(): write_data(data) # fake function for demo mark_data_copied() # fake function for demo
不管有多少个 App,只要主动去 Push 它的 App Context,Context Stack 中就会累积起来。这样,栈顶永远是当前操做的 App Context。当一个 App Context 结束的时候,相应的栈顶元素也随之出栈。若是在执行过程当中抛出了异常,对应的 App Context 中注册的 teardown 函数被传入带有异常信息的参数。
这么一来就解释了两个疑问 —— 在这种单线程运行环境中,只有栈结构才能保存多个 Context 并在其中定位出哪一个才是“当前”。而离线脚本只须要 App 关联的上下文,不须要构造出请求,因此 App Context 也应该和 Request Context 分离。
另外一个手动推入 Context 的场景是测试。测试中咱们可能会须要构造一个请求,并验证相关的状态是否符合预期。例如:
def test_app(): app = create_app() client = app.test_client() resp = client.get('/') assert 'Home' in resp.data
这里调用 client.get 时,Request Context 就被推入了。其特色和 App Context 很是相似,这里再也不赘述。
[1] | Flask 文档对 Application Context 和 Request Context 做出了详尽的解释; |
[2] | 经过访问 flask.current_app; |
[3] | 经过访问 flask.request; |
[4] | Flask(Werkzeug) 的 Context 基于 Thread Local 和代理模式实现,只要身处 Context 中就能用近似访问全局变量的的方式访问到上下文信息,例如 flask.current_app 和 flask.request;Django 和 Tornado 则将上下文封装在对象中,只有明确获取了相关上下文对象才能访问其中的信息,例如在视图函数中或按照规定模板实现的 Middleware 中; |
[5] | 基于 Flask 的 Web 应用能够在 Gevent 或 Eventlet 异步网络库 patch 过的 Python 环境中正常工做。这两者都使用 Greenlet 而不是系统线程做为调度单元,而 Werkzeug 考虑到了这点,在 Greenlet 可用时用 Greenlet ID 代替线程 ID。 |
[6] | Python 的对象方法是 Descriptior 实现的,因此方法就是一种属性;而 Python 的二元操做能够用双下划线开头和结尾的一系列协议,因此 foo + bar 等同于 foo.__add__(bar),本质仍是属性访问。 |
[7] | Flask-Script 是一个用来写 manage.py 管理脚本的 Flask 扩展,用它运行的任务会在开始前自动推入 App Context。未来这个“运行任务”的功能将被整合到 Flask 内部。 |
[8] | 详见 Flask 源码中的 setup_method 装饰器 |
推送程序上下文:app = Flask(xxx), app.app_context().push() 推送了程序上下文,g可使用,当前线程的current_app指向app