Flask 上下文(Context)原理解析

Flask 上下文(Context)原理解析:

Flask提供了两种上下文,一种是应用上下文(Application Context),一种是请求上下文(Request Context)。其中 Application 表示用户响应 WSGI 请求的应用本身,即采用类似 app = Flask(name) 创建的对象。Request 则代表每次 HTTP 请求,其实质是 WSGI Server(例如 gunicorn)调用该 Flask.call() 后,于 Flask 对象内部创建的 Request 对象。

1.上下文(Context)相关概念
a.Thread Local 概念
从面向对象设计的角度看,对象是保存“状态”的地方。Python 也是如此,一个对象的状态都被保存在对象携带的一个特殊字典中,可以通过 vars 函数拿到它。
Thread Local 则是一种特殊的对象,它的“状态”对线程隔离,也就是说每个线程对一个 Thread Local 对象的修改都不会影响其他线程。这种对象的实现原理也非常简单,只要以线程的 ID 来保存多份状态字典即可,就像按照门牌号隔开的一格一格的信箱。在 Python 中获得一个这样的 Thread Local 最简单的方法是 threading.local(),如下即展示了 Thread 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
这样来说,只要能构造出 Thread Local 对象,就能够让同一个对象在多个线程下做到状态隔离。这个“线程”不一定要是系统线程,也可以是用户代码中的其他调度单元,例如 Greenlet(基于 Flask 的 Web 应用可以在 Gevent 或 Eventlet 异步网络库 patch 过的 Python 环境中正常工作。这二者都使用 Greenlet 而不是系统线程作为调度单元,而 Werkzeug 考虑到了这点,在 Greenlet 可用时用 Greenlet ID 代替线程 ID。)。
b.Werkzeug 实现的 Local Stack 和 Local Proxy
Werkzeug 没有直接使用 threading.local,而是自己实现了 werkzeug.local.Local 类。后者和前者有一些区别:
(1).后者会在 Greenlet 可用的情况下优先使用 Greenlet 的 ID 而不是线程 ID 以支持 Gevent 或 Eventlet 的调度,前者只支持多线程调度;
(2).后者实现了 Werkzeug 定义的协议方法 __release_local__,可以被 Werkzeug 自己的 release_pool 函数释放(析构)掉当前线程下的状态,前者没有这个能力。
除 Local 外,Werkzeug 还实现了两种数据结构:LocalStack 和 LocalProxy。
①.LocalStack
LocalStack 是用 Local 实现的栈结构,可以将对象推入、弹出,也可以快速拿到栈顶对象。当然,所有的修改都只在本线程可见。和 Local 一样,LocalStack 也同样实现了支持 release_pool 的接口。
②.LocalProxy
LocalProxy 则是一个典型的代理模式实现,它在构造时接受一个 callable 的参数(比如一个函数),这个参数被调用后的返回值本身应该是一个 Thread Local 对象。对一个 LocalProxy 对象的所有操作,包括属性访问、方法调用(当然方法调用就是属性访问)甚至是二元操作(Python 的对象方法是 Descriptior 实现的,所以方法就是一种属性;而 Python 的二元操作可以用双下划线开头和结尾的一系列协议,所以 foo + bar 等同于 foo.__add__(bar),本质还是属性访问。)都会转发到那个 callable 参数返回的 Thread Local 对象上。
LocalProxy 的一个使用场景是 LocalStack 的 __call__ 方法。比如 my_local_stack 是一个 LocalStack 实例,那么 my_local_stack() 能返回一个 LocalProxy 对象,这个对象始终指向 my_local_stack 的栈顶元素。如果栈顶元素不存在,访问这个 LocalProxy 的时候会抛出 RuntimeError
2.上下文(Context)原理
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 对象也是请求间隔离的。
a.请求上下文(Request Context)
在每个请求上下文的函数中我们都可以访问 Request 对象,然而 Request 对象却并不是全局的。例如创建一个包含 Request 对象的函数如下,当运行时则将报错 RuntimeError: working outside of request context
def handle_request():
    print 'handle request'
    print request.url 
if __name__=='__main__':
    handle_request()
因此可知,Flask 的 Request 对象只有在其上下文的生命周期内才有效,离开了请求的生命周期,其上下文环境不存在了,也就无法获取 Request 对象了。可以使用 Flask 的内部方法 request_context() 来构建一个请求上下文,如下所示。
from werkzeug.test import EnvironBuilder
ctx = app.request_context(EnvironBuilder('/','http://localhost/').get_environ())
ctx.push()
try:
    print request.url
finally:
    ctx.pop()
对于 Flask Web 应用来说,每个请求就是一个独立的线程。请求之间的信息要完全隔离,避免冲突。因此当每个请求进入的时候,都将请求上下文对象压栈入 Flask._request_ctx_stack 中。上下文压入栈后,再次请求的时候都是通过 _request_ctx_stack.top 在栈的顶端取出,所取到的永远是属于自己线程的对象,这样不同线程之间的上下文就做到了隔离。请求结束后,线程退出,Thread Local 本地变量也随即销毁,然后调用 ctx.pop() 弹出上下文对象并回收内存。
b.应用上下文(Application 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-SQLAlchemy 写成的 Model 上调用 User.query.get(user_id),就会遇到 RuntimeError因为此时 App Context 还没被推入栈中,而 Flask-SQLAlchemy 需要数据库连接信息时就会去取 current_app.configcurrent_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>
current_app 只能在请求线程里存在,因此它的生命周期也是在应用上下文里,离开了应用上下文也就无法使用。如下例所示,离开了应用上下文环境,current_app 的调用也会出现"RuntimeError: working outside of application context"。
app = Flask('__name__')
print current_app.name
同样可以手动创建应用上下文:
with app.app_context():
    print current_app.name
注意:这里的 with 语句和 with open() as f 一样,可以为提供上下文环境省略简化一部分工作,这里就简化了其压栈和出栈操作。
注意:当 Flask App 在作为 WSGI Application 运行时,会在每个请求进入的时候将请求上下文推入 _request_ctx_stack 中,而请求上下文一定是应用上下文之中,所以推入部分的逻辑有这样一条:如果发现 _app_ctx_stack 为空,则隐式地推入一个 App 上下文。这便是应用运行时不需要手动 app_context().push() 的原因。最终,在请求线程退出前,应用上下文将从其 Flask._app_ctx_stack 栈中里弹出

3.上下文(Context)设计理念

a.区分应用上下文及请求上下文,并采用堆栈保存上下文的原因。
由于 App Context 和 Request Context 都是 Thread Local 的,所以区分应用及请求上下文的原因往往难以理解。同时,在 Web 应用运行时中,一个线程同时只处理一个请求,那么 _req_ctx_stack_app_ctx_stack 肯定都是只有一个栈顶元素,所以起采用堆栈方式保存上下文同样存在疑问。
实际上,Flask 是为了提供多个 Flask App 共存非 Web Runtime 中灵活控制 Context 的可能性。
①.多个 Flask App 共存
多数情况下,一个 Flask App 调用 app.run() 之后,进程就进入阻塞模式并开始监听请求。此时是不可能再让另一个 Flask App 在主线程运行起来的。但是 WSGI Middleware 是允许使用组合模式的,即使多个 App 同时运行,如下例程序所示。
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 上处理
注意:这种用法和 Flask 的 Blueprint 是有区别的。Blueprint 虽然和这种用法很类似,但前者自己没有 App Context,只是同一个 Flask App 内部整理资源的一种方式,所以多个 Blueprint 可能共享了同一个 Flask App;后者面向的是所有 WSGI Application,而不仅仅是 Flask App,即使是把一个 Django App 和一个 Flask App 用这种用法整合起来也是可行的。
②.非 Web Runtime 中灵活控制 Context
Flask App 不一定仅仅在 Web Runtime 中被使用,有两个典型的场景是在非 Web 环境需要访问上下文代码的,一个是离线脚本,另一个是测试,这两个场景即所谓的“Running code outside of a request”。
例如:一个离线脚本需要操作两个 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()
        with admin_app.app_context():
            write_data(data)
        mark_data_copied()
例子中,无论有多少个 App,只要主动去 Push 它的 App Context,Context Stack 中就会累积起来。这样,栈顶永远是当前操作的 App Context。当一个 App Context 结束的时候,相应的栈顶元素也随之出栈。如果在执行过程中抛出了异常,对应的 App Context 中注册的 teardown 函数被传入带有异常信息的参数。
posted on 2017-03-13 15:07  autopenguin  阅读(1392)  评论(0编辑  收藏  举报