什么是上下文膜蠢?
每一段程序都有很多外部變量。只有像Add這種簡單的函數(shù)才是沒有外部變量的莉兰。一旦你的一段程序有了外部變量挑围,這段程序就不完整,不能獨立運行糖荒。你為了使他們運行杉辙,就要給所有的外部變量一個一個寫一些值進去。這些值的集合就叫上下文捶朵。
上下文作用域蜘矢?
舉個例子:你有一個應用函數(shù)返回用戶應該跳轉(zhuǎn)到的 URL 。想象它總是會跳轉(zhuǎn)到 URL 的 next 參數(shù)综看,或 HTTP referrer 品腹,或索引頁:
from flask import request, url_for
def redirect_url():
return request.args.get('next') or \
request.referrer or \
url_for('index')
我們訪問了請求的對象,但是當你運行程序時候:
>>> redirect_url()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'request'
出現(xiàn)這個錯誤红碑,因為我們當前并沒有可以訪問的請求舞吭。所以我們需要制造一個請求并且綁定到當前的上下文。test_request_context方法為我們創(chuàng)建了一個RequestContext:
>>> ctx = app.test_request_context('/?next=http://example.com/')
可以通過兩種方式利用這個上下文:使用 with 聲明或是調(diào)用push()和pop()方法:
>>> ctx.push()
然后我們使用請求對象:
>>> redirect_url()
u'http://example.com/'
直到我們調(diào)用pop:
>>> ctx.pop()
因為請求上下文在內(nèi)部作為一個棧來維護析珊,所以你可以多次壓棧出棧羡鸥。這在實現(xiàn)內(nèi)部重定向之類的東西時很方便。
上下文如何工作的唾琼?
我們看下這段代碼:
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對象兄春,并結(jié)合with聲明來綁定上下文。從相同線程中被調(diào)用的一切锡溯,直到with
聲明結(jié)束前赶舆,都可以訪問全局的請求變量flask.request和其它)。
請求上下文內(nèi)部工作如同一個棧祭饭。棧頂是當前活動的請求芜茵。push把上下文添加到棧頂,pop把它移出棧倡蝙。在出棧時九串,應用的teardown_request函數(shù)也會被執(zhí)行。
另一件需要注意的事是,請求上下文被壓入棧時猪钮,并且沒有當前應用的應用上下文品山,它會自動創(chuàng)建一個 應用上下文 。
請求上下文
當Flask應用真正處理請求時烤低,wsgi_app(environ, start_response)被調(diào)用肘交。這個函數(shù)是按照下面的方式運行的:
def wsgi_app(environ, start_response):
with self.request_context(environ):
...
在Flask中處理請求時,應用會生成一個“請求上下文”對象扑馁。整個請求的處理過程涯呻,都會在這個上下文對象中進行。這保證了請求的處理過程不被干擾腻要。
看段代碼:
# Flask v0.1
class _RequestContext(object):
"""The request context contains all request relevant information. It is
created at the beginning of the request and pushed to the
`_request_ctx_stack` and removed at the end of it. It will create the
URL adapter and request object for the WSGI environment provided.
"""
def __init__(self, app, environ):
self.app = app
self.url_adapter = app.url_map.bind_to_environ(environ)
self.request = app.request_class(environ)
self.session = app.open_session(self.request)
self.g = _RequestGlobals()
self.flashes = None
def __enter__(self):
_request_ctx_stack.push(self)
def __exit__(self, exc_type, exc_value, tb):
# do not pop the request stack if we are in debug mode and an
# exception happened. This will allow the debugger to still
# access the request object in the interactive shell.
if tb is None or not self.app.debug:
_request_ctx_stack.pop()
根據(jù)_RequestContext上下文對象的定義复罐,可以發(fā)現(xiàn),在構(gòu)造這個對象的時候添加了和Flask應用相關(guān)的一些屬性:
app ——上下文對象的app屬性是當前的Flask應用雄家;
url_adapter ——上下文對象的url_adapter屬性是通過Flask應用中的Map實例構(gòu)造成一個MapAdapter實例效诅,主要功能是將請求中的URL和Map實例中的URL規(guī)則進行匹配;
request ——上下文對象的request屬性是通過Request類構(gòu)造的實例咳短,反映請求的信息填帽;
session ——上下文對象的session屬性存儲請求的會話信息;
g ——上下文對象的g屬性可以存儲全局的一些變量咙好。
flashes ——消息閃現(xiàn)的信息篡腌。
def wsgi_app(self, environ, start_response):
with self.request_context(environ):
# with語句中生成一個`response`對象
...
return response(environ, start_response)
請求上下文對象包含了和請求處理相關(guān)的信息。同時Flask還根據(jù)werkzeug.local模塊中實現(xiàn)的一種數(shù)據(jù)結(jié)構(gòu)LocalStack用來存儲“請求上下文”對象勾效。
- LocalStack詳解
>>> from werkzeug.local import LocalStack
>>> import threading
# 創(chuàng)建一個`LocalStack`對象
>>> local_stack = LocalStack()
# 查看local_stack中存儲的信息
>>> local_stack._local.__storage__
{}
# 定義一個函數(shù)嘹悼,這個函數(shù)可以向`LocalStack`中添加數(shù)據(jù)
>>> def worker(i):
local_stack.push(i)
# 使用3個線程運行函數(shù)`worker`
>>> for i in range(3):
t = threading.Thread(target=worker, args=(i,))
t.start()
# 再次查看local_stack中存儲的信息
>>> local_stack._local.__storage__
{<greenlet.greenlet at 0x4bee5a0>: {'stack': [2]},
<greenlet.greenlet at 0x4bee638>: {'stack': [1]},
<greenlet.greenlet at 0x4bee6d0>: {'stack': [0]}
}
由上面的例子可以看出,存儲在LocalStack中的信息以字典的形式存在:鍵為線程/協(xié)程的標識數(shù)值层宫,值也是字典形式杨伙。每當有一個線程/協(xié)程上要將一個對象push進LocalStack棧中,會形成如上一個“鍵-值”對萌腿。這樣的一種結(jié)構(gòu)很好地實現(xiàn)了線程/協(xié)程的隔離限匣,每個線程/協(xié)程都會根據(jù)自己線程/協(xié)程的標識數(shù)值確定存儲在棧結(jié)構(gòu)中的值。
LocalStack還實現(xiàn)了push毁菱、pop米死、top等方法。其中top方法永遠指向棧頂?shù)脑刂印m數(shù)脑厥侵府斍熬€程/協(xié)程中最后被推入棧中的元素峦筒,即local_stack._local.stack-1。
local模塊
應用上下文
Flask 背后的設計理念之一就是窗慎,代碼在執(zhí)行時會處于兩種不同的“狀態(tài)”(states)物喷。當Flask對象被實例化后在模塊層次上應用便開始隱式地處于應用配置狀態(tài)卤材。一直到第一個請求還是到達這種狀態(tài)才隱式地結(jié)束。當應用處于這個狀態(tài)的時候峦失,你可以認為下面的假設是成立的:
- 程序員可以安全地修改應用對象
- 目前還沒有處理任何請求
- 你必須得有一個指向應用對象的引用來修改它扇丛。不會有某個神奇的代理變量指向你剛創(chuàng)建的或者正在修改的應用對象的
相反,到了第二個狀態(tài)宠进,在處理請求時晕拆,有一些其它的規(guī)則:
- 當一個請求激活時,上下文的本地對象flask.request和其它對象等)指向當前的請求
- 你可以在任何時間里使用任何代碼與這些對象通信
這里有一個第三種情況材蹬,有一點點差異。有時吝镣,你正在用類似請求處理時方式來與應用交互堤器,即使并沒有活動的請求。想象一下你用交互式 Python shell 與應用交互的情況末贾,或是一個命令行應用的情況闸溃。
current_app上下文本地變量就是應用上下文驅(qū)動的。
應用上下文的作用
應用上下問存在的主要原因是拱撵,在過去辉川,請求上下文被附加了一堆函數(shù),但是又沒有什么好的解決方案拴测。因為 Flask 設計的支柱之一是你可以在一個 Python 進程中擁有多個應用乓旗。
那么代碼如何找到“正確的”應用?在過去集索,我們推薦顯式地到處傳遞應用屿愚,但是這會讓我們在使用不是以這種理念設計的庫時遇到問題。
解決上述問題的常用方法是使用后面將會提到的 current_app代理對象务荆,它被綁定到當前請求的應用的引用妆距。既然無論如何在沒有請求時創(chuàng)建一個這樣的請求上下文是一個沒有必要的昂貴操作,應用上下文就被引入了函匕。
創(chuàng)建應用上下文
有兩種方式來創(chuàng)建應用上下文娱据。第一種是隱式的:無論何時當一個請求上下文被壓棧時,如果有必要的話一個應用上下文會被一起創(chuàng)建盅惜。由于這個原因中剩,你可以忽略應用上下文的存在,除非你需要它酷窥。
第二種是顯式地調(diào)用 app_context()方法:
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
在配置了SERVER_NAME時咽安,應用上下文也被用于 url_for()函數(shù)。這允許你在沒有請求時生成 URL 蓬推。
應用上下文局部變量
應用上下文會在必要時被創(chuàng)建和銷毀妆棒。它不會在線程間移動,并且也不會在不同的請求之間共享。正因為如此糕珊,它是一個存儲數(shù)據(jù)庫連接信息或是別的東西的最佳位置动分。內(nèi)部的棧對象叫做 flask._app_ctx_stack。擴展可以在最頂層自由地存儲額外信息红选,想象一下它們用一個充分獨特的名字在那里存儲信息澜公,而不是在 flask.g對象里, flask.g 是留給用戶的代碼用的喇肋。
上下文用法
上下文的一個典型應用場景就是用來緩存一些我們需要在發(fā)生請求之前或者要使用的資源坟乾。舉個例子,比如數(shù)據(jù)庫連接蝶防。當我們在應用上下文中來存儲東西的時候你得選擇一個唯一的名字甚侣,這是因為應用上下文為 Flask 應用和擴展所共享。
最常見的應用就是把資源的管理分成如下兩個部分:
- 一個緩存在上下文中的隱式資源
- 當上下文被銷毀時重新分配基礎資源
通常來講间学,這將會有一個 get_X() 函數(shù)來創(chuàng)建資源 X 殷费,如果它還不存在的話。 存在的話就直接返回它低葫。另外還會有一個 teardown_X() 的回調(diào)函數(shù)用于銷毀資源 X 详羡。
如下是我們剛剛提到的連接數(shù)據(jù)庫的例子:
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()
當get_db()這個函數(shù)第一次被調(diào)用的時候數(shù)據(jù)庫連接已經(jīng)被建立了。為了使得看起來更隱式一點我們可以使用 LocalProxy這個類:
from werkzeug.local import LocalProxydb = LocalProxy(get_db)
這樣的話用戶就可以直接通過訪問db來獲取數(shù)據(jù)句柄了嘿悬,db已經(jīng)在內(nèi)部完成了對get_db()的調(diào)用实柠。