Python Web Flask原始碼解讀(四)——全域性變數
關於我
一個有思想的程式猿,終身學習實踐者,目前在一個創業團隊任team lead,技術棧涉及Android、Python、Java和Go,這個也是我們團隊的主要技術棧。
Github:https://github.com/hylinux1024
微信公眾號:終身開發者(angrycode)
Flask
中全域性變數有current_app
、request
、g
和session
。不過需要注意的是雖然標題是寫著全域性變數,但實際上這些變數都跟當前請求的上下文環境有關,下面一起來看看。
current_app
是當前啟用程式的應用例項;request
是請求物件,封裝了客戶端發出的HTTP
請求中的內容;g
是處理請求時用作臨時儲存的物件,每次請求都會重設這個變數;session
0x00 current_app
應用程式上下文可用於跟蹤一個請求過程中的應用程式例項。可以像使用全域性變數一樣直接匯入就可以使用 (注意這個變數並不是全域性變數)。
Flask
例項有許多屬性,例如config
可以Flask
進行配置。
一般在建立Flask
例項時
from flask import Flask
app = Flask(__name__)
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
...
通常不會直接匯入app
這個變數,而是使用通過匯入current_app
from flask import current_app
current_app 的生命週期
Flask
應用在處理客戶端請求(request
)時,會在當前處理請求的執行緒中推送(push
)一個上下文例項和請求例項(request
),請求結束時就會彈出(pop
)請求例項和上下文例項,所以current_app
和request
是具有相同的生命週期的,且是繫結在當前處理請求的執行緒上的。
如果一個沒有推送上下文例項就直接使用current_app
,會報錯
RuntimeError: Working outside of application context. This typically means that you attempted to use functionality that needed to interface with the current application object in some way. To solve this, set up an application context with app.app_context().
如果要直接使用current_app
就要手動推送(push
)應用上下文例項,從上面的錯誤資訊可以知道,可以使用with
語句,幫助我們push
一個上下文例項
def create_app():
app = Flask(__name__)
with app.app_context():
init_db()
return app
需要注意的是current_app
是“執行緒”本地變數,所以current_app
需要在檢視函式或命令列函式中使用,否則也會報錯。
要理解這一點就要對伺服器程式工作機制有所瞭解。一般伺服器程式都是多執行緒程式,它會維護一個執行緒池,對於每個請求,伺服器會從執行緒池中獲取一個執行緒用於處理這個客戶端的請求,而應用的current_app
、request
等變數是“執行緒”本地變數,它們是繫結在“執行緒”中的(相當於執行緒自己獨立的記憶體空間),所以也線上程環境下才能夠使用。
在Flask
中是否也是通過執行緒本地變數來實現的呢?這個問題我們在後面的工作原理一節會給出答案。
0x01 g
若要在應用上下文中儲存資料,Flask
提供了g
這個變數為我們達到這個目的。g
其實就是global
的縮寫,它的生命週期是跟應用上下文的生命週期是一樣的。
例如在一次請求中會多次查詢資料庫,可以把這個資料庫連線例項儲存在當次請求的g
變數中,在應用上下文生命週期結束關閉連線。
from flask import g
def get_db():
if 'db' not in g:
g.db = connect_to_database()
return g.db
@app.teardown_appcontext
def teardown_db():
db = g.pop('db', None)
if db is not None:
db.close()
0x02 request
request
封裝了客戶端的HTTP
請求,它也是一個執行緒本地變數。
沒有把這個變數放在處理api
請求的函式中,而是通過執行緒本地變數進行封裝,極大地方便使用,以及也使得程式碼更加簡潔。
request
的生命週期是跟current_app
是一樣的,從請求開始時建立到請求結束銷燬。同樣地Flask
在處理請求時就會push
一個request
和應用上下文的代理例項,然後才可以使用。如果沒有push
就使用就會報錯
RuntimeError: Working outside of request context.
This typically means that you attempted to use functionality that
needed an active HTTP request. Consult the documentation on testing
for information about how to avoid this problem.
通常這個錯誤在測試程式碼中會經常遇到,如果需要在單元測試中使用request
,可以使用test_client
或者在with
語句中使用test_requet_context()
進行模擬
def generate_report(year):
format = request.args.get('format')
...
with app.test_request_context(
'/make_report/2017', data={'format': 'short'}):
generate_report()
0x03 session
前面講到如果在一個請求期間共享資料,可以使用g
變數,但如果要在不同的請求(request
)之間共享資料,那就需要使用session
,這是一個私有儲存的字典型別。可以像操作字典一樣操作session
。
session
是使用者會話,可以儲存請求之間的資料。例如在使用login
介面進行使用者登入之後,把使用者登入資訊儲存在session
中,然後訪問其它介面時就可以通過session
獲取到使用者的登入資訊。
@app.route('/login')
def login():
# 省略登入操作
...
session['user_id']=userinfo
@app.route('/show')
def showuser():
# 省略其它操作
...
userid = request.args.get('user_id')
userinfo = session.get(userid)
0x04 工作原理
我們知道Flask
在處理一個請求時,wsgi_app()
這個方法會被執行。而在Flask
的原始碼內部request
和current_app
是通過_request_ctx_stack
這個棧結構來儲存的,分別為
# context locals
_request_ctx_stack = LocalStack()
current_app = LocalProxy(lambda: _request_ctx_stack.top.app)
request = LocalProxy(lambda: _request_ctx_stack.top.request)
session = LocalProxy(lambda: _request_ctx_stack.top.session)
g = LocalProxy(lambda: _request_ctx_stack.top.g)
需要注意最新的版本原始碼會有些不同request
和current_app
分別是有兩個棧結構來儲存:_request_ctx_stack
和_app_ctx_stack
。但新舊程式碼思路是差不多的。
最新的原始碼裡,全域性變數的定義
# 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"))
其中_find_app
和_lookup_app_object
方法是這樣定義的
def _find_app():
top = _app_ctx_stack.top
if top is None:
raise RuntimeError(_app_ctx_err_msg)
return top.app
def _lookup_req_object(name):
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)
可以看到current_app
和g
是LocalProxy
通過_app_ctx_stack.top
進行封裝的。request
和session
是_request_ctx_stack
的封裝。LocalProxy
是werkzeug
庫中local
物件的代理。LocalStack
顧名思義是一個實現了棧的資料結構。
前面提到全域性變數是跟執行緒繫結的,每個執行緒都有一個獨立的記憶體空間,在A
執行緒設定的變數,在B
執行緒是無法獲取的,只有在A
執行緒中才能獲取到這個變數。這個在Python
的標準庫有thread locals
的概念。
然而在Python
中除了執行緒外還有程序和協程可以處理併發程式的技術。所以為了解決這個問題Flask
的依賴庫werkzeug
就實現了自己的本地變數werkzeug.local
。它的工作機制跟執行緒本地變數(thread locals
)是類似的。
要使用werkzug.local
from werkzeug.local import Local, LocalManager
local = Local()
local_manager = LocalManager([local])
def application(environ, start_response):
local.request = request = Request(environ)
...
application = local_manager.make_middleware(application)
在application(environ,start_response)
方法中就把封裝了請求資訊的request
變數繫結到了local變數中。然後在相同的上下文下例如在一次請求期間,就可以通過local.request
來獲取到這個請求對應的request
資訊。
同時還可以看到LocalManager
這個類,它是本地變數管理器,它可以確保在請求結束之後及時的清理本地變數資訊。
在原始碼中對LocalManager
是這樣註釋的
Local objects cannot manage themselves. For that you need a local
manager. You can pass a local manager multiple locals or add them later
by appending them tomanager.locals
. Every time the manager cleans up,
it will clean up all the data left in the locals for this context.
Local
不能自我管理,需要藉助LocalManager
這個管家來實現請求結束後的清理工作。
0x05 總結
current_app
、g
、request
和session
是Flask
中常見4個全域性變數。current_app
是當前Flask
服務執行的例項,g
用於在應用上下文期間儲存資料的變數,request
封裝了客戶端的請求資訊,session
代表了使用者會話資訊。
0x06 學習資料
- https://werkzeug.palletsprojects.com/en/0.15.x/local/