什么是上下文?
每一段程序都有很多外部變量。只有像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ù)脑?。棧頂?shù)脑厥侵府斍熬€程/協(xié)程中最后被推入棧中的元素,即local_stack._local.stack-1。
local模塊
應用上下文
Flask 背后的設(shè)計理念之一就是,代碼在執(zhí)行時會處于兩種不同的“狀態(tài)”(states)。當Flask對象被實例化后在模塊層次上應用便開始隱式地處于應用配置狀態(tài)。一直到第一個請求還是到達這種狀態(tài)才隱式地結(jié)束。當應用處于這個狀態(tài)的時候,你可以認為下面的假設(shè)是成立的:
- 程序員可以安全地修改應用對象
- 目前還沒有處理任何請求
- 你必須得有一個指向應用對象的引用來修改它。不會有某個神奇的代理變量指向你剛創(chuàng)建的或者正在修改的應用對象的
相反,到了第二個狀態(tài),在處理請求時,有一些其它的規(guī)則:
- 當一個請求激活時,上下文的本地對象flask.request和其它對象等)指向當前的請求
- 你可以在任何時間里使用任何代碼與這些對象通信
這里有一個第三種情況,有一點點差異。有時,你正在用類似請求處理時方式來與應用交互,即使并沒有活動的請求。想象一下你用交互式 Python shell 與應用交互的情況,或是一個命令行應用的情況。
current_app上下文本地變量就是應用上下文驅(qū)動的。
應用上下文的作用
應用上下問存在的主要原因是,在過去,請求上下文被附加了一堆函數(shù),但是又沒有什么好的解決方案。因為 Flask 設(shè)計的支柱之一是你可以在一個 Python 進程中擁有多個應用。
那么代碼如何找到“正確的”應用?在過去,我們推薦顯式地到處傳遞應用,但是這會讓我們在使用不是以這種理念設(shè)計的庫時遇到問題。
解決上述問題的常用方法是使用后面將會提到的 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 應用和擴展所共享。
最常見的應用就是把資源的管理分成如下兩個部分:
- 一個緩存在上下文中的隱式資源
- 當上下文被銷毀時重新分配基礎(chǔ)資源
通常來講,這將會有一個 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)用。