1. 程式人生 > >Python Web Flask原始碼解讀(四)——全域性變數

Python Web Flask原始碼解讀(四)——全域性變數

關於我
一個有思想的程式猿,終身學習實踐者,目前在一個創業團隊任team lead,技術棧涉及Android、Python、Java和Go,這個也是我們團隊的主要技術棧。
Github:https://github.com/hylinux1024
微信公眾號:終身開發者(angrycode)

Flask中全域性變數有current_apprequestgsession。不過需要注意的是雖然標題是寫著全域性變數,但實際上這些變數都跟當前請求的上下文環境有關,下面一起來看看。

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_apprequest是具有相同的生命週期的,且是繫結在當前處理請求的執行緒上的。

如果一個沒有推送上下文例項就直接使用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_apprequest等變數是“執行緒”本地變數,它們是繫結在“執行緒”中的(相當於執行緒自己獨立的記憶體空間),所以也線上程環境下才能夠使用。

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的原始碼內部requestcurrent_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)

需要注意最新的版本原始碼會有些不同requestcurrent_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_appgLocalProxy通過_app_ctx_stack.top進行封裝的。requestsession_request_ctx_stack的封裝。LocalProxywerkzeug庫中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 to manager.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_appgrequestsessionFlask中常見4個全域性變數。current_app是當前Flask服務執行的例項,g用於在應用上下文期間儲存資料的變數,request封裝了客戶端的請求資訊,session代表了使用者會話資訊。

0x06 學習資料

  • https://werkzeug.palletsprojects.com/en/0.15.x/local/