你们新年好!鉴于今年春晚很是好看,我以为承受不起,因而来写点辣鸡水文娱乐下你们,这也是以前立的若干 Flag 中的一个web
作过 Flask 开发的朋友都知道 Flask 中存在着两个概念,一个叫 App Context , 一个叫 Request Context 。 这两个算是 Flask 中很独特的一种机制。sql
从一个 Flask App 读入配置并启动开始,就进入了 App Context,在其中咱们能够访问配置文件、打开资源文件、经过路由规则反向构造 URL。当 WSGI Middleware 调用 Flask App 的时候开始,就进入了 Request Context 。咱们能够获取到其中的 HTTP HEADER 等操做,同时也能够进行 SESSION 等操做。数据库
不过做为辣鸡选手而言,常常分不清为何会存在这两个 Context ,没事,咱们慢慢来讲一说。flask
首先要清楚一点,咱们要在同一个进程中隔离不一样线程的数据,那么咱们会优先选择 threading.local
,来实现数据彼此隔离的需求。可是如今有个问题来了,如今咱们并发模型可能并非只有传统意义上的进程-线程模型。也有多是 coroutine(协程) 模型。常见的就是 Greenlet/Eventlet 。在这种状况下,threading.local
就无法很好的知足咱们的需求。因而 Werkzeug 实现了本身的 Local 即 werkzeug.local.Local
session
那么 Werkzeug 本身实现的 Local 和标准的 threading.local
相比有什么不一样呢?咱们记住最大的不一样点在于数据结构
前者会在 Greenlet 可用的状况下优先使用 Greenlet 的 ID 而不是线程 ID 以支持 Gevent 或 Eventlet 的调度,后者只支持多线程调度;多线程
Werkzeug 另外还实现了两种数据结构,一个叫 LocalStack
,一个叫作 LocalProxy
并发
LocalStack
是基于 Local
实现的一个栈结构。栈的特性就是后入先出。当咱们进入一个 Context 时,将当前的的对象推入栈中。而后咱们也能够获取到栈顶元素。从而获取到当前的上下文信息。app
LocalProxy
是代理模式的一种实现。在实例化的时候,传入一个 callable
的参数。而后这个参数被调用后将会返回一个 Local
对象。咱们后续的全部操做,好比属性调用,数值计算等,都会转发到这个参数返回的 Local
对象上。函数
如今你们可能不太清楚,咱们为何要用 LocalProxy 来进行操做,咱们来给你们看一个例子
from werkzeug.local import LocalStack
test_stack = LocalStack()
test_stack.push({'abc': '123'})
test_stack.push({'abc': '1234'})
def get_item():
return test_stack.pop()
item = get_item()
print(item['abc'])
print(item['abc'])
复制代码
你看咱们这里的输出的的值,都是统一的 1234
,可是咱们这里想作到的是每次获取的值都是栈顶的最新的元素,那么咱们这个时候就应该用 proxy 模式了
from werkzeug.local import LocalStack, LocalProxy
test_stack = LocalStack()
test_stack.push({'abc': '123'})
test_stack.push({'abc': '1234'})
def get_item():
return test_stack.pop()
item = LocalProxy(get_item)
print(item['abc'])
print(item['abc'])
复制代码
你看咱们这里就是 Proxy 的妙用。
因为 Flask 基于 Werkzeug 实现,所以 App Context 以及 Request Context 是基于前文中所说的 LocalStack 实现。
从命名上,你们应该能够看出,App Context 是表明应用上下文,可能包含各类配置信息,好比日志配置,数据库配置等。而 Request Context 表明一个请求上下文,咱们能够获取到当前请求中的各类信息。好比 body 携带的信息。
这两个上下文的定义是在 flask.ctx 文件中,分别是 AppContext
以及 RequestContext
。而构建上下文的操做则是将其推入在 flask.globals 文件中定义的 _app_ctx_stack
以及 _request_ctx_stack
中。前面说了 LocalStack 是“线程”(这里多是传统意义上的线程,也有多是 Greenlet 这种)隔离的。同时 Flask 每一个线程只处理一个请求,所以能够作到请求隔离。
当 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>
复制代码
做为 web 时,当请求进来时,咱们开始进行上下文的相关操做。整个流程以下:
好了如今有点问题:
为何要区分 App Context 以及 Request Context
为何要用栈结构来实现 Context ?
好久以前看过的松鼠奥利奥老师的博文Flask 的 Context 机制 解答了这个问题
这两个作法给予咱们 多个 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 上处理。
可是如今不少朋友有个问题,就是为何这里不用 Blueprint ?
Blueprint 是在同一个 App 下运行。其挂在 App Context 上的相关信息都是一致的。可是若是要隔离彼此的信息的话,那么用 App Context 进行隔离,会比咱们用变量名什么的隔离更为方便
Middleware 模式是 WSGI 中容许的特性,换句话来说,咱们将 Flask 和另一个遵循 WSGI 协议的 web Framework (好比 Django)那么也是可行的。
可是 Flask 的两种 Context 分离更大的意义是为了非 web 应用的场合。Flask 官方文档中有这样一段话
The main reason for the application’s context existence is that in the past a bunch of functionality was attached to the request context for lack of a better solution. Since one of the pillars of Flask’s design is that you can have more than one application in the same Python process.
这句话换句话说 App Context 存在的意义是针对一个进程中有多个 Flask App 场景,这样场景最多见的就是咱们用 Flask 来作一些离线脚本的代码。
好了,咱们来聊聊 Flask 非 Web 应用的场景
好比,咱们有个插件叫 Flask-SQLAlchemy 而后这里有个使用场景 首先咱们如今有这样一个代码
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
database = Flask(__name__)
database.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
db = SQLAlchemy(database)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
def __repr__(self):
return '<User %r>' % self.username
复制代码
这里你应该注意到最开始的几个关键点,第一个,就是 database.config
,是的没错,Flask-SQLAlchemy 就是从当前的 app 中获取到对应的 config 信息来创建数据库连接。那么传递 app 的方式有两种,第一种,就是直接如上图同样,直接 db = SQLAlchemy(database) ,这个很容易理解,第二种,若是咱们不传的话,那么 Flask-SQLAlchemy 中经过 current_app 来获取当前的 app 而后获取对应的 config 创建连接。 那么问题来了,为何会存在第二种这种方法呢
给个场景吧,如今我两个数据库配置不一样的 app 共用一个 Model 那么应该怎么作?其实很简单
首先写 一个 model 文件,好比就叫 data/user_model.py 吧
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
def __repr__(self):
return '<User %r>' % self.username
复制代码
好了,那么在咱们的应用文件中,咱们即可以这样写
from data.user_model import User
database = Flask(__name__)
database.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
with database.app_context():
db.init_app(current_app)
db.create_all()
admin = User(username='admin', email='admin@example.com')
db.session.add(admin)
db.session.commit()
print(User.query.filter_by(username="admin").first())
database1 = Flask(__name__)
database1.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test1.db'
with database1.app_context():
db.init_app(current_app)
db.create_all()
admin = User(username='admin_test', email='admin@example.com')
db.session.add(admin)
db.session.commit()
print(User.query.filter_by(username="admin").first())
复制代码
你看这样是否是就好懂了一些,经过 app context ,咱们 Flask-SQLAlchemy 能够经过 current_app 来获取当前 app ,继而获取相关的 config 信息
这个例子还不够稳当,咱们如今再来换一个例子
from flask import Flask, current_app
import logging
app = Flask("app1")
app2 = Flask("app2")
app.config.logger = logging.getLogger("app1.logger")
app2.config.logger = logging.getLogger("app2.logger")
app.logger.addHandler(logging.FileHandler("app_log.txt"))
app2.logger.addHandler(logging.FileHandler("app2_log.txt"))
with app.app_context():
with app2.app_context():
try:
raise ValueError("app2 error")
except Exception as e:
current_app.config.logger.exception(e)
try:
raise ValueError("app1 error")
except Exception as e:
current_app.config.logger.exception(e)
复制代码
好了,这段代码很清晰了,含义很清晰,就是经过获取当前上下文中的 app 中的 logger 来输出日志。同时这段代码也很清晰的说明了,咱们为何要用栈这样一种数据结构来维护上下文。
首先看一下 app_context()
的源码
def app_context(self):
"""Binds the application only. For as long as the application is bound to the current context the :data:`flask.current_app` points to that application. An application context is automatically created when a request context is pushed if necessary. Example usage:: with app.app_context(): ... .. versionadded:: 0.9 """
return AppContext(self)
复制代码
嗯,很简单,只是构建一个 AppContext 对象返回,而后咱们看看相关的代码
class AppContext(object):
"""The application context binds an application object implicitly to the current thread or greenlet, similar to how the :class:`RequestContext` binds request information. The application context is also implicitly created if a request context is created but the application is not on top of the individual application context. """
def __init__(self, app):
self.app = app
self.url_adapter = app.create_url_adapter(None)
self.g = app.app_ctx_globals_class()
# Like request context, app contexts can be pushed multiple times
# but there a basic "refcount" is enough to track them.
self._refcnt = 0
def push(self):
"""Binds the app context to the current context."""
self._refcnt += 1
if hasattr(sys, 'exc_clear'):
sys.exc_clear()
_app_ctx_stack.push(self)
appcontext_pushed.send(self.app)
def pop(self, exc=_sentinel):
"""Pops the app context."""
try:
self._refcnt -= 1
if self._refcnt <= 0:
if exc is _sentinel:
exc = sys.exc_info()[1]
self.app.do_teardown_appcontext(exc)
finally:
rv = _app_ctx_stack.pop()
assert rv is self, 'Popped wrong app context. (%r instead of %r)' \
% (rv, self)
appcontext_popped.send(self.app)
def __enter__(self):
self.push()
return self
def __exit__(self, exc_type, exc_value, tb):
self.pop(exc_value)
if BROKEN_PYPY_CTXMGR_EXIT and exc_type is not None:
reraise(exc_type, exc_value, tb)
复制代码
emmmm,首先 push
方法就是将本身推入 _app_ctx_stack
,而 pop
方法则是将本身从栈顶推出。而后咱们看到两个方法含义就很明确了,在进入上下文管理器的时候,将本身推入栈,而后退出上下文管理器的时候,将本身推出。
咱们都知道栈的一个性质就是,后入先出,栈顶的永远是最新插入进去的元素。而看一下咱们 current_app
的源码
def _find_app():
top = _app_ctx_stack.top
if top is None:
raise RuntimeError(_app_ctx_err_msg)
return top.app
current_app = LocalProxy(_find_app)
复制代码
嗯,很明了了,就是获取当前栈顶的元素,而后进行相关操做。
嗯,经过这样对于栈的不断操做,就能让 current_app
获取到元素是咱们当前上下文中的 app 。
g 也是咱们经常使用的几个全局变量之一。在最开始这个变量是挂载在 Request Context 下的。可是在 0.10 之后,g 就是挂载在 App Context 下的。可能有同窗不太清楚为何要这么作。
首先,说一下 g 用来干什么
官方在上下文这一张里有这一段说明
The application context is created and destroyed as necessary. It never moves between threads and it will not be shared between requests. As such it is the perfect place to store database connection information and other things. The internal stack object is called flask._app_ctx_stack. Extensions are free to store additional information on the topmost level, assuming they pick a sufficiently unique name and should put their information there, instead of on the flask.g object which is reserved for user code.
大意就是说,数据库配置和其他的重要配置信息,就挂载 App 对象上。可是若是是一些用户代码,好比你不想一层层函数传数据的话,而后有一些变量须要传递,那么能够挂在 g 上。
同时前面说了,Flask 并不只仅能够当作一个 Web Framework 使用,同时也能够用于一些非 web 的场合下。在这种状况下,若是 g 是属于 Request Context 的话,那么咱们要使用 g 的话,那么就须要手动构建一个请求,这无疑是不合理的。
大年三十写这篇文章,如今发出来,个人辣鸡也是无人可救了。Flask 的上下文机制是其最重要的特性之一。经过合理的利用上下文机制,咱们能够再更多的场合下去更好的利用 flask 。嗯,本次的辣鸡文章写做活动就到此结束吧。但愿你们不会扔我臭鸡蛋!而后新年快乐!