[回答者Mark Hildreth]
Multiple Apps 多个应用
Flask可以有多个应用,如果没有了解到这一点,应用上下文的作用确实会令人迷惑。考虑一下这种场景:你想在一个WSGI python解释器运行多个Flask应用。这里我们讲的不是蓝本,而是完全不同的Flask应用。
一个应用分发(Application Dispatching)的例子
from werkzeug.wsgi import DispatcherMiddleware
from frontend_app import application as frontend
from backend_app import application as backend
application = DispatcherMiddleware(frontend, {
'/backend': backend
})
注意,这里有两个完全不同的Flask应用:frontend与backend(前端与后端)
换句话说,通过调用两次Flask(…)的构造方法来创建两个flask应用实例
Contexts 上下文
当你使用Flask时,需要经常使用全局变量访问不同的函数。例如,你可能会读到下边的代码:
from flask import request
在某个视图中,你可能会使用request对象访问当前请求的信息。显然,request不是全局变量。实际上,它是一个context local值。这里做了一个特殊处理,使得当我们请求request.path时能正确的从当前request的对象中获取path属性。
实际上,即使在Flask中运行多个线程,Flask也能够保持request对象的隔离性。在这种情况下,不同的线程可以处理各自的请求,并发的获取request.path信息,而且确保在他们各自的请求中能获取到正确的信息。
Putting it Together 综合考虑
我们已经看到Flask可以在同一个解释器中处理多个应用,这是由于Flask允许你使用”context local”全局变量来实现的。这其中肯定是有某种机制决定究竟哪个是当前的request对象。
综合以上情况,Flask一定有某种方法判断当前的应用是哪个。
你可能会碰到如下代码:
from flask import url_for
与request例子类似,url_for函数逻辑上是取决于当前环境。在这种情形下,可以十分肯定的说这种逻辑与Flask认定哪个app是当前的app有着紧密的关系。在上边的frontend/backend例子中,frontend与backend的app中可能都包含/login路由,url_for(‘/login’)会为不同app返回不同的值,这取决于视图正在处理的request(frontend或backend)
To answer your questions… 书归正传,回答问题
当我们谈到request或者应用上下文时,栈的目的是什么?
Request Context 文档:
由于在request上下文中维护者一个栈,因此你可以多次地出栈和入栈。这种方式十分有利于实现内部的重定向转发。
换句话说,你可以将多个内部的重定向请求放入“当前”requests或“当前”应用的栈中。
下边给出一个例子,你可以使request返回“内部重定向”的结果。例如一个用户对A发起请求,并将它返回的结果传给用户B。大多数情况下,你会给这个用户生成一个重定向请求,将这个用户重新定向到资源B,这意味着用户要运行第二个请求去获取B。与此对比。略微有些不同的处理方式是使用内部重定向,当处理A时,Flask会为资源B本身生成一个新的请求,将第二个请求的结果作为用户原始请求的结果。(笔者附注:不同体现在,第一种方法是程序员自己创建第二个request请求,而第二种是Flask框架自动创建这种request,并返回正确的结果给我们)
问题:这两个栈是分别独立的还是他们是一个栈的两部分?
他们是两个独立的栈。在任何时候你可以获得“当前”的应用或请求(位于栈的顶部)。源码:
flask.globals.py
……
#contextlocals
_request_ctx_stack=LocalStack() #request 上下文栈
_app_ctx_stack=LocalStack() # Flask应用上下文栈
问题:request上下文是被推进栈中,还是其本身就是一个栈?
一个请求上下文是request 上下文栈(_request_ctx_stack)的元素。“应用上下文”与”app context stack”之间的关系与之类似。
问题:可以在两个栈的顶部push/pop多个上下文吗?如果可以,何种情况下需要这么做?
在Flask应用中,你通常不需要这么做。举个例子,你可能会在处理内部重定向时想要这么做。但是,即使在这种情形下,你也应该利用Flask处理新请求,Flask会为你做好所有的push/pop操作。
有一些情形下你需要自己进行栈操作
Running code outside of a request 在请求之外运行代码
一个典型的例子是 ,当使用 Flask-SQLAlchemy扩展设置SQL数据库或者定义模型时。当使用类似下方的代码时,
app = Flask(__name__)
db = SQLAlchemy() # Initialize the Flask-SQLAlchemy extension object
db.init_app(app)
我们需要在shell中使用app和db的值时:
from myapp import app, db
# Set up models
db.create_all()
在这种情况下,Flask-SQLAlchemy扩展需要知道app应用的信息,但是当执行create_all()时,会抛出一个没有上下文的错误。
RuntimeError: application not registered on db instance and no application bound to current context
这个错误是正常的,这是由于在运行create_all()时,你没有告诉Flask这个应用需要处理的信息。
你可能疑惑,为何在视图中你运行相似的函数,没有使用with app.app_context()也没有出现错误呢。原因是,当你处理实际的web请求时,flask已经替你自动管理好应用的上下文。此类问题只会出现在:当有代码运行在视图函数(或类似的回调函数)以外的情形下。
解决方案是自己将应用上下文推入栈中,例如:
from myapp import app, db
# Set up models
with app.app_context():
db.create_all()
测试
另一个需要手动操作栈的地方是测试。你可以创建一个单元测试用来处理请求和检查结果:
import unittest
from flask import request
class MyTest(unittest.TestCase):
def test_thing(self):
with app.test_request_context('/?next=http://example.com/') as ctx:
# 你可以在这查看请求上下文栈的属性
#此处请求上下文栈会是空的
[回答者mike_e]
每个http请求都要创建一个上下文(线程),这是必须创建Local线程的原因。这样可以通过维护特定的request上下文,来保证request和g这样的对象可以被全局访问。此外,Flask在处理Http请求时,可以从内部模拟request,这就必须要求在一个栈中存储他们各自的上下文。Flask允许多个WSGI应用运行在单一进程中,并且在一个request请求中可能调用不止一个应用,因此必须为应用设计一个上下文栈。
我们首先来理解一下werkzeug如何实现Local线程。
Local
当发起一个 http请求时,某个线程的上下文会处理这个请求。也就是说,在http请求发起的同时会产生一个新的上下文。Werkzeug(__version__='0.12.2')
允许使用greenlets替代python的原生线程。如果没有安装greenlets会使用threads代替。每个线程都有一个唯一id作为标志符,get_ident()函数提供线程的检索功能。这个函数隐藏在request, current_app,url_for, g,等上下文绑定的全局对象中。
try:
from greenlet import get_ident
except ImportError:
from thread import get_ident
通过线程检索函数get_ident,我们可以很容易的知道当前的线程。我们可以创建一个叫做Local的线程,Local是一个上下文对象,可以被全局的访问。你可以访问特定线程的属性值。
例如:
# globally
local = Local()
# ...
# on thread 1
local.first_name = 'John'
# ...
# on thread 2
local.first_name = 'Debbie'
在同一时刻,可以通过访问Local来获取这两个线程的属性值。当查询local.first_name时,线程1的上下文会返回’John’,而线程2会返回’Debbie’
这是如何做到的呢?我们看一下Local的源码:
werkzeug.local.py
class Local(object):
__slots__ = ('__storage__', '__ident_func__')
def __init__(self):
# 初始化线程字典__storage__,键:线程id,值:线程
object.__setattr__(self, '__storage__', {})
#get_ident 函数生成线程的Id
object.__setattr__(self, '__ident_func__', get_ident)
def __release_local__(self):
self.__storage__.pop(self.__ident_func__(), None)
def __getattr__(self, name):
try:
return self.__storage__[self.__ident_func__()][name]
except KeyError:
raise AttributeError(name)
def __setattr__(self, name, value):
ident = self.__ident_func__()
storage = self.__storage__
try:
storage[ident][name] = value #更新属性值
except KeyError:
storage[ident] = {name: value} # 设置属性值
从上面的代码可以看到,Local使用字典storage存储线程和相应的线程id.
键:线程id,值:线程。初始化时绑定字典和线程id生成函数。getattr是从字典中根据id取出线程。 setattr将特定的线程放入字典中(或者更新已有的值)。在Flask中并没有使用Local对象,使用的是LocalProxy 对象。
LocalProxy
class LocalProxy(object):
def __init__(self, local, name):
# local这里是一个实际的Local对象,可以用来查找特定的对象,其标识符是name.
#local是可以调用的,可以确定代理对象
self.local = local
# 'name'作为标识符,传递给local来查找特定的对象
self.name = name
def _get_current_object(self):
#如果self.local是一个Local对象,则其已经实现了__release_local__()方法,
#正如其名字一样,通常用来释放Local对象
#这里通过简单的查找来标记哪个是实际的Local对象,哪个是可调用的对象
if hasattr(self.local, '__release_local__'):
try:
return getattr(self.local, self.name)
except AttributeError:
raise RuntimeError('no object bound to %s' % self.name)
#如果self.local不是一个Local对象,则一定是可调用的对象
#,这样可以决定用户感兴趣的对象
return self.local(self.name)
#现在LocalProxy 执行其特定的职责
#比如,在Local中代理一个对象,我们将感兴趣的对象的魔幻方法
#全部交给代理来处理
@property
def __dict__(self):
try:
return self._get_current_object().__dict__
except RuntimeError:
raise AttributeError('__dict__')
def __repr__(self):
try:
return repr(self._get_current_object())
except RuntimeError:
return '<%s unbound>' % self.__class__.__name__
def __bool__(self):
try:
return bool(self._get_current_object())
except RuntimeError:
return False
# ... 等等 ...
def __getattr__(self, name):
if name == '__members__':
return dir(self._get_current_object())
return getattr(self._get_current_object(), name)
def __setitem__(self, key, value):
self._get_current_object()[key] = value
def __delitem__(self, key):
del self._get_current_object()[key]
# ... 等等 ...
__setattr__ = lambda x, n, v:
setattr(x._get_current_object(), n, v)
__delattr__ = lambda x, n:
delattr(x._get_current_object(), n)
__str__ = lambda x: str(x._get_current_object())
__lt__ = lambda x, o: x._get_current_object() < o
__le__ = lambda x, o: x._get_current_object() <= o
__eq__ = lambda x, o: x._get_current_object() == o
# ... 等等 …
现在你可以这样创建全局访问代理对象
# this would happen some time near application start-up
local = Local()
request = LocalProxy(local, 'request')
g = LocalProxy(local, 'g')
在request请求的前期,你可以在local(之前创建的代理)中存储一些对象,无论哪个线程都可以访问:
# this would happen early during processing of an http request
local.request = RequestContext(http_environment)
local.g = SomeGeneralPurposeContainer()
使用LocalProxy 为全局访问对象,相对直接使用Local对象而言,可以简化对象管理。你可以为一个单一的Local对象创建许多全局代理对象。在request请求的末期(清理阶段),你可以简单的释放一个Local(对其storage执行pop操作),并且这种操作不会影响代理,这些代理对象仍然可以全局的访问,并处理随后的http请求。
# this would happen some time near the end of request processing
release(local) # aka local.__release_local__()
当我们已经有一个Local对象时,为了简化创建代理LocalProxy ,Werkzeug 实现Local.call()方法的过程如下:
class Local(object):
# ...
# ... all same stuff as before go here ...
# ...
def __call__(self, name):
return LocalProxy(self, name)
# now you can do
local = Local()
request = local('request')
g = local('g')
然而,如果查看Flask源码(flask.globals.py),你仍然无法知道request, g, current_app 和session等对象时如何创建。当我们创建应用时,Flask会同时创建多个”假的”request请求(从一个真正的http请求)并且在过程中push多个应用上下文。由于这些”并发”的请求和应用在任一时刻只有一个被处理。因此,有必要使用一个栈来储存他们各自的上下文。当一个新的请求产生或者一个应用被调用时,他们会将上下文push进各自的栈中。Flask使用LocalStack目的正是如此。当他们结束自己的业务时会从栈中弹出上下文。
LocalStack
class LocalStack(object):
def __init__(self):
self.local = Local()
def push(self, obj):
"""压入栈中一个新的元素"""
rv = getattr(self.local, 'stack', None)
if rv is None:
self.local.stack = rv = []
rv.append(obj)
return rv
def pop(self):
"""移除栈顶的元素, 返回旧值或None.
"""
stack = getattr(self.local, 'stack', None)
if stack is None:
return None
elif len(stack) == 1:
release_local(self.local) # 释放 local
return stack[-1]
else:
return stack.pop()
@property
def top(self):
"""T获取栈顶的元素 """
try:
return self.local.stack[-1]
except (AttributeError, IndexError):
return None
注意,LocalStack是一个驻存在Local对象里的栈,并不是存储Local对象的栈。这意味着,尽管这个栈是全局可以访问的,但是其在彼此的线程中是不同的。
Flask并没有直接的从LocalStack中获取request, current_app, g, 和session等对象,而是包装了一层查找功能来寻找潜在的对象。
class LocalStack(object):
…..
"""__version__='0.12.2'"""
def __call__(self):
def _lookup():
rv = self.top
if rv is None:
raise RuntimeError('object unbound')
return rv
return LocalProxy(_lookup)
…
#flask.globals.py [__version__='0.12.2']
def _lookup_req_object(name): #在request上下文栈中查找属性
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)
request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))
g = LocalProxy(partial(_lookup_app_object, 'g'))
所有的上述对象,在应用建立的开始,栈中不会有任何的对象。除非你将一个request或者应用的上下文压入到他们各自的栈中。
如果你对上下文是如何插入栈中的细节感兴趣,可以参考源码flask.app.Flask.wsgi_app()。当登录到wsgi的应用时web服务器将http环境参数传递给request请求,随后创建RequestContext 对象。然后调用push()方法将上下文压入_request_ctx_stack栈中。一旦push到栈的顶部,就可以全局的访问_request_ctx_stack.top。这里列出上述流程的部分代码:
建立一个WSGI应用:
app = Flask(*config, **kwconfig)
# ...
随后http请求到达服务器,WSGI服务器调用app:
app(environ, start_response) # aka app.__call__(environ, start_response)
app中大致的处理过程如下:
class Flask(object):
# ...
def __call__(self, environ, start_response):
return self.wsgi_app(environ, start_response)
def wsgi_app(self, environ, start_response):
ctx = RequestContext(self, environ)
ctx.push()
try:
# process the request here
# raise error if any
# return Response
finally:
ctx.pop()
# …
随后RequestContext处理代码如下:
class RequestContext(object):
def __init__(self, app, environ, request=None):
self.app = app
if request is None:
request = app.request_class(environ)
self.request = request
self.url_adapter = app.create_url_adapter(self.request)
self.session = self.app.open_session(self.request)
if self.session is None:
self.session = self.app.make_null_session()
self.flashes = None
def push(self):
_request_ctx_stack.push(self)
def pop(self):
_request_ctx_stack.pop()
当request 请求完成初始化后,接着在视图函数功能中查找request.path。随后的处理步骤如下:
• 流程的起点是全局可访问的LocalProxy 对象
• 在_lookup_req_object函数中查找特定的对象
• _lookup_req_object函数在_request_ctx_stack栈的顶部查找对象
• 为了查找顶部的上下文,LocalStack 对象首先检索内部的Local属性(self._local),并设置Local的stack属性
• 从stack中获取顶top的上下文
• top.request可以决定客户端感兴趣的对象
• 从这个对象中获取path属性
我们已经知道Local, LocalProxy, 和 LocalStack的工作机制,现在总结一下:
request 对象是一个简单的全局可访问的对象。它是一个代理对象,储存在Local对象的属性stack中,即存储在stack的栈顶。