Flask中的Context包括App Context和Request Context,从一个 Flask App 读入配置并启动开始,就进入了 App Context。当一个请求进入开始被处理时,就进入了 Request Context,在其中我们可以访问请求携带的信息。

Thread Local

从面向对象设计的角度看,对象是保存“状态”的地方。一个对象的状态都被保存在对象携带的一个特殊字典中,可以通过 vars 函数拿到它。

Thread Local 则是一种特殊的对象,它的“状态”对线程隔离 —— 也就是说每个线程对一个 Thread Local 对象的修改都不会影响其他线程。本地线程希望不同的线程对于内容的修改只在线程内发挥作用,线程之前互不影响。实现的原理就是在threading.current_thread().__dict__里面添加一个包含对象ID的key,每一个线程保存不同的状态字典,所以每一个线程中的值都不一样。

在 Python 中获得一个这样的 Thread Local 最简单的方法是threading.local()

所以只要能构造出 Thread Local 对象,就能够让同一个对象在多个线程下做到状态隔离。

Werkzeug实现的本地线程

Werkzeug自己实现了本地线程,werkzeug.local.Local和threading.local的区别:

  1. werkzeug使用了自定义的__storage__保存不同线程下的状态
  2. werkzeug提供了释放本地线程的release_local方法,可以被 Werkzeug 自己的 release_pool 函数释放(析构)掉当前线程下的状态
  3. Werkzeug使用get_ident函数,用来获得线程’/协程标识符
  4. werkzeug会在 Greenlet 可用的情况下优先使用 Greenlet 的 ID 而不是线程 ID 以支持 Gevent 或 Eventlet 的调度,threading.local只支持多线程调度

除了Local外,Werkzeug 还实现了两种数据结构:LocalStack 和 LocalProxy。

LocalStack

LocalStack 是用 Local (werkzeug的本地线程)实现的栈结构,可以将对象推入、弹出,也可以快速拿到栈顶对象。所有的修改都只在本线程可见。和 Local 一样,LocalStack 也同样实现了支持 release_pool 的接口。

LocalProxy

是一个典型的代理模式实现,它在构造时接受一个 callable 的参数(比如一个函数),这个参数被调用后的返回值本身应该是一个 Thread Local 对象,即通过LocalStack实例化的栈的栈顶对象。对一个 LocalProxy 对象的所有操作,包括属性访问、方法调用(当然方法调用就是属性访问)甚至是二元操作都会转发到那个 callable 参数返回的 Thread Local 对象上。

Flask 基于 Local Stack 的 Context

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 状态。所以编写离线脚本的时候,必须将App的App Context推入栈中,栈顶不为空,则current_app就是一个Local Proxy对象,就可以将动作转发到App上了。

应用运行的时候不需要手动app_context().push(),因为 Flask App 在作为 WSGI Application 运行时,会在每个请求进入的时候将请求上下文推入 _request_ctx_stack 中,而请求上下文一定是 App 上下文之中,所以推入部分的逻辑有这样一条:如果发现 _app_ctx_stack为空,则隐式地推入一个 App 上下文。

App Context和Request Context两者独立

一个 Flask App 实例就是一个 WSGI Application,那么 WSGI Middleware 是允许使用组合模式的

1
2
3
4
5
6
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()
})

这样将两个 Flask App 组合成一个一个 WSGI Application。这种情况下两个 App 都同时在运行,只是根据 URL 的不同而将请求分发到不同的 App 上处理。多个 Blueprint 可能共享了同一个 Flask App;而这种面向的是所有 WSGI Application,而不仅仅是 Flask App,即使是把一个 Django App 和一个 Flask App 用这种用法整合起来也是可行的。

在web运行中,多个Flask App可以同时工作是因为每个请求被处理的时候是身处不同的Thread Local中的,也就是Web Runtime情况中使用。下面是非Web环境中需要访问上下文代码的。Running code outside of a request

离线脚本或者测试运行Flask关联的代码

这种情况,一般只在主线程中运行,但是如果需要操作两个 Flask App 关联的上下文,这时候栈结构的 App Context就很有用。

1
2
3
4
5
6
7
8
9
10
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 关联的上下文,不需要构造出请求,所以 App Context 也应该和 Request Context 分离

测试中我们可能会需要构造一个请求,并验证相关的状态是否符合预期。

1
2
3
4
5
def test_app():
app = create_app()
client = app.test_client()
resp = client.get('/')
assert 'Home' in resp.data

这里调用 client.get 时,Request Context 就被推入了。

App Factory

尽量使用 App Factory 模式比较好。况且配合 Blueprint 的情况下,App Factory 还能帮助我们良好地组织应用结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from flask import Flask
from werkzeug.utils import import_string
extensions = [
'happytree.ext:db',
'happytree.ext:login_manager',
]
blueprints = [
'happytree.views:bp',
]
def create_app():
app = Flask(__name__)
for ext_name in extensions:
ext = import_string(ext_name)
ext.init_app(app)
for bp_name in blueprints:
bp = import_string(bp_name)
app.register_blueprint(bp)
return app

详解应用上下文

当 Flask 对象被实例化后在模块层次上应用便开始隐式地处于应用配置状态。一直到第一个请求到达这种状态才隐式地结束。这样使用上下文把某些对象变成全局可访问,实际上是特定环境的局部对象的代理,每个线程看到的上下文对象是不通的。

在应用处于应用配置状态的时候:

  • 可以安全地修改应用对象
  • 目前还没有处理任何请求
  • 必须得有一个指向应用对象的引用来修改它

到了第二个状态,在处理请求的时候,相当于已经申请到了应用上下文和请求上下文

  • 当一个请求激活时,上下文的本地对象( flask.request 和其它对象等) 指向当前的请求
  • 可以在任何时间里使用任何代码与这些对象通信

current_app 上下文本地变量就是应用上下文驱动的。

应用上下文的作用

Flask 设计的支柱之一是你可以在一个 Python 进程中拥有多个应用。代码如何找到“正确的”应用,常用方法是使用后面将会提到的 current_app 代理对象,它被绑定到当前请求的应用的引用。在没有请求时创建一个这样的请求上下文是一个没有必要的昂贵操作,应用上下文就被引入了。

创建应用上下文

有两种方式来创建应用上下文。

  1. 在当一个请求上下文被压栈的时候,如果发现 _app_ctx_stack为空,则隐式地推入一个 App 上下文。
  2. 显式的调用app_context()方法
1
2
3
4
5
from flask import Flask, current_app
app = Flask(__name__)
with app.app_context():
# within this block, current_app points to app.
print current_app.name

应用上下文局部变量

应用上下文会在必要时被创建和销毁。它不会在线程间移动,并且也不会在不同的请求之间共享。它是一个存储数据库连接信息或是别的东西的最佳位置。内部的栈对象叫做 flask._app_ctx_stack 。扩展可以在最顶层自由地存储额外信息

上下文用法

一个典型应用场景就是用来缓存一些我们需要在发生请求之前或者要使用的资源。比如数据库连接。在应用上下文中来存储东西的时候你得选择一个唯一的名字,这是因为应用上下文为 Flask 应用和扩展所共享。

最常见的应用就是把资源的管理分成如下两个部分:

  1. 一个缓存在上下文中的隐式资源
  2. 当上下文被销毁时候重新分配基础资源

比如数据库连接,将会有一个 get_X() 函数来创建资源 X ,如果它还不存在的话。 存在的话就直接返回它。另外还会有一个 teardown_X() 的回调函数用于销毁资源 X。其实Flask扩展中也是如此。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import sqlite3
from flask import g
def get_db():
db = getattr(g, '_database', None)
if db is None:
db = g._database = connect_to_database()
return db
@app.teardown_appcontext
def teardown_db(exception):
db = getattr(g, '_database', None)
if db is not None:
db.close()
from werkzeug.local import LocalProxy
db = LocalProxy(get_db)

这样,可以在应用上下文中直接通过访问 db 来获取数据句柄了,db已经在内部完成了对get_db()的调用。

Flask扩展开发

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
import sqlite3
from flask import current_app
# Find the stack on which we want to store the database connection.
# Starting with Flask 0.9, the _app_ctx_stack is the correct one,
# before that we need to use the _request_ctx_stack.
try:
from flask import _app_ctx_stack as stack
except ImportError:
from flask import _request_ctx_stack as stack
class SQLite3(object):
def __init__(self, app=None):
self.app = app
truetruetruetrue# 接受一个应用对象,如果有,则调用init_app
if app is not None:
self.init_app(app)
def init_app(self, app):
truetruetruetrue# 没有分配app到self,只在应用传递到构造函数时在对象上存储应用
truetruetruetrue# 默认一个内存的数据库
app.config.setdefault('SQLITE3_DATABASE', ':memory:')
# Use the newstyle teardown_appcontext if it's available,
# otherwise fall back to the request context
if hasattr(app, 'teardown_appcontext'):
truetruetruetruetruetrue# 使用应用上下文处理器,如果不存在退回到上下文处理器
app.teardown_appcontext(self.teardown)
else:
app.teardown_request(self.teardown)
def connect(self):
truetruetruetrue# 打开一个数据库连接
return sqlite3.connect(current_app.config['SQLITE3_DATABASE'])
def teardown(self, exception):
ctx = stack.top
if hasattr(ctx, 'sqlite3_db'):
ctx.sqlite3_db.close()
@property
def connection(self):
ctx = stack.top
if ctx is not None:
truetruetruetrue # 首次访问时候打开数据库连接,将其存储在上下文
truetruetruetruetruetrue# 这是处理资源的推荐方式,在资源第一次使用时候惰性获取资源
if not hasattr(ctx, 'sqlite3_db'):
ctx.sqlite3_db = self.connect()
return ctx.sqlite3_db

这里通过_app_ctx_stack.top 附加到应用上下文的栈顶。扩展应该使用上下文的栈顶来存储它们自己的信息,并使用足够复杂的名称,也就实现了在应用上下文中对数据库连接的管理了。

可以这样使用:

1
2
3
4
5
6
7
8
9
10
11
12
from flask import Flask
from flask_sqlite3 import SQLite3
app = Flask(__name__)
app.config.from_pyfile('the-config.cfg')
db = SQLite3(app)
@app.route('/')
def show_all():
cur = db.connection.cursor()
cur.execute(...)
with app.app_context():
cur = db.connection.cursor()
cur.execute(...)

请求上下文

request.args.get('next')这访问了请求对象,如果没有请求上下文,那么就没有可以访问的请求,所以就会出错。需要制造一个请求并且绑定到当前的上下文, test_request_context 方法为我们创建一个 RequestContext:ctx = app.test_request_context('/?next=http://example.com/')

使用这个上下文,使用with声明或者是调用push()和pop方法

1
2
3
ctx.push()
redirect_url()
ctx.pop()

因为请求上下文在内部作为一个栈来维护,所以你可以多次压栈出栈。这在实现内部重定向之类的东西时很方便。

上下文如何工作

1
2
3
4
5
6
7
def wsgi_app(self, environ):
with self.request_context(environ):
try:
response = self.full_dispatch_request()
except Exception, e:
response = self.make_response(self.handle_exception(e))
return response(environ, start_response)

request_context() 方法返回一个新的 RequestContext 对象,并结合 with 声明来绑定上下文。在这个上下文中就可以访问全局的请求变量比如flask.request,请求上下文内部工作如同一个栈。栈顶是当前活动的请求。 push() 把上下文添加到栈顶, pop() 把它移出栈。在出栈时,应用的 teardown_request() 函数也会被执行。请求上下文被压入栈时,并且没有当前应用的应用上下文, 它会自动创建一个 应用上下文 。

请求的一个处理过程和错误处理

  1. 在请求之前,执行before_request()上绑定的函数,如果这个函数返回了一个响应,那么这个返回值就会代替视图函数的返回值,并且视图函数不会被调用
  2. 如果 before_request() 上绑定的函数没有返回一个响应, 常规的请求处理将会生效,匹配的视图函数有机会返回一个响应。
  3. 视图的返回值之后会被转换成一个实际的响应对象,并交给 after_request() 上绑定的函数适当地替换或修改它。
  4. 请求的最后,会执行teardonw_request()上绑定的函数。这是一定会发生的。

在生产模式中,如果一个异常没有被捕获,将调用 500 internal server 的处理。在生产模式中,即便异常没有被处理过,也会往上冒泡抛给给 WSGI 服务器。

销毁回调

当请求上下文出栈时, teardown_request() 上绑定的函数会被调用。销毁回调总是会被执行,即使没有请求前回调执行过,或是异常发生。在with声明中,或者在命令行中使用请求上下文的时候,其寿命会被延长,直到with声明结束,或者出栈,在此之前有可能一个请求已经完毕。

代理

Flask 中提供的一些对象是其它对象的代理。这些代理在线程间共享, 并且它们在必要的情景中被调度到限定在一个线程中。

代理对象不会伪造它们继承的类型,所以如果你想运行真正的实例检查,你需要在被代理的实例上这么做(见下面的 _get_current_object )。对象引用是重要的,比如发送信号。

需要访问潜在的被代理的对象,你可以使用 _get_current_object() 方法

1
2
app = current_app._get_current_object()
my_signal.send(app)

使用上下文

典型应用场景是缓存一些在发生请求之前要使用的资源,比如数据库连接和缓存一些对象。请求上下文发生在HTTP请求开始,WSGI Server调用Flask.__call__()之后。

1
2
3
4
5
6
7
8
9
10
class RequestContext(object):
self._implicit_app_ctx_stack = []
truedef push(self):
true app_ctx = _app_ctx_stack.top
truetrue# 栈顶为空,或者栈顶app对象不是当前的上下文请求app对象
truetrueif app_ctx is None or app_ctx.app != self.app:
truetrue # 进入当前应用上下文
truetrue app_ctx = self.app.app_context()
truetruetrueapp_ctx.push()
truetruetrueself._implicit_app_ctx_stack.append(app_ctx)

从此可以看出应用上下文是被动的在推入请求上下文过程中生成的,在请求结束的时候也会把请求上下文弹出。所以变量的名字就是隐式的应用上下文栈。

1
2
3
4
5
6
7
8
class RequestContext(object):
def pop(self, exc=_sentinel):
true app_ctx = self._implicit_app_ctx_stack.pop()
truetruetry:
truetrue # some
truetruefinally:
truetrue if app_ctx is not None:
truetruetrue app_ctx.pop(exc)

应用上下文和请求上下文分开的目的是:首先有可能出现多app,app之前也要隔离,然后是非Web模式下,一个应用上下文可能有多个请求上下文,但是退出一个请求上下文不能将应用上下文也直接退出了。

flask有4个上下文变量

  1. flask.current_app: 应用上下文,当前app实例对象
  2. flask.g: 应用上下文,处理请求时用作临时存储的对象
  3. flask.request: 请求上下文,封装了客户端发出的HTTP请求中的内容
  4. flask.session: 请求上下文,存储了用户会话

有这么几个钩子装饰器

  • before_first_request
  • before_request
  • teardown_appcontext: 不管是否有异常,注册的函数都会在每次请求之后执行
  • context_processor: 上下文处理的装饰器,返回的字典中的键可以在上下文中使用,其中对于模版,就可以将上下文资源传递进去,就可以用这些函数了。
  • after_request
1
2
3
@app.context_processor
def template_extras():
return {'enumerate': enumerate, 'current_user': g.user}

使用LocalProxy代替g

实现一个全局可以访问的current_user

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from werkzeug.local import LocalStack, LocalProxy
_user_stack = LocalStack()
def get_current_user():
top = _user_stack.top
trueif top is None:
true raise RuntimeError()
truereturn top
current_user = LocalProxy(get_current_user)
@app.before_request
def before_request():
users = User.query.all()
trueuser = random.choice(users)
true_user_stack.push(user)
@app.teardown_appcontext
def teardown(exc=None):
_user_stack.pop()

相当于每次请求中,要访问current_user就会在这个独立的线程来执行get_current_user函数来获取栈顶对象,另外堆栈也是线程独立的。所以在请求上下文中就可以直接访问current_user了。所以可以按照这种方法将一些全局使用的资源放进去,数据库连接是肯定可以放的。

所以之前战斗力项目中,app=LocalProxy(get_app)其实就是多app支持的啊!多app有什么特性呢?等待继续研究。

参考:

  1. https://blog.tonyseek.com/post/the-context-mechanism-of-flask/#id12
  2. 应用上下文 - Flask官方文档
  3. 请求上下文 - Flask官方文档
  4. 董伟明 Python Web开发实战