[回答者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的棧頂。