Flask中本地棧的使用

金色旭光發表於2022-02-18

4種上下文變數

承接上一篇內容。當一個請求到來時,除了request被封裝成全域性變數之外,還有三個變數也是同樣被封裝成全域性變數,那就是current_app、g、session。上面4個變數之所以能夠使用,是因為程式上下文生效了。

上下文這個概念非常常見,比如在程式切換時時會儲存當前程式的上下文,恢復活動程式的上下文。我見過對上下文對通透的解釋就是說所謂上下文就是執行環境,恢復上下文就是恢復執行環境。

在Flask中有兩種上下文:程式上下文請求上下文。當一個請求到來時,Flask會啟用這兩種上下文,其中request就是在請求上下文中獲取。

變數名 上下文 說明
current_app 應用上下文 當前啟用程式的程式例項
g 應用上下文 處理請求時用作臨時儲存的物件。每次請求都會重設這個變數
request 請求上下文 請求物件,封裝了客戶端發出的HTTP請求中的內容
session 請求上下文 使用者會話,用於儲存請求之間需要“記住”的字典

上下文在請求中的生命週期

Flask在分發請求之前將程式上下文(AppContext)壓入應用本地棧中,將請求上下文(RequestContext)壓入請求本地棧中。請求處理完成後再將兩個上下文分別出棧。
程式上下文被入棧後,就可以在檢視函式中使用current_app和g變數;類似的,請求上下文被推送後,就可以使用request和session變數。

具體來說:請求上下文儲存在_request_ctx_stack,程式上下文儲存在 _app_ctx_stack。當一個請求到來時,請求上下文物件RequestContext,程式上下文物件AppContext都會相應入棧。

Flask經典錯誤

如果我們沒有啟用程式上下文或請求上下文就使用這些變數會導致一個Flask的經典錯誤。
RuntimeError: Working outside of application context.
比如:

from flask import Flask, current_app

app = Flask(__name__)
print(current_app.name)

在上面的示例中列印current_app的名字,但是並沒有在檢視函式中,也就是說沒有請求到來。那麼這一段程式就會報錯:

這就是因為current_app必須是要在請求到來時,請求上下文和程式上下文都啟用之後才能使用。上面一段程式碼沒有請求到來,所以current_app不能使用。

手動壓棧程式上下文

為了能夠在沒有請求到來也能使用Flask專案的配置,檔案等,可以手動將程式上下文入棧。

from flask import Flask, current_app

app = Flask(__name__)

with app.app_context():
    print(current_app)
    print(current_app.config) # 列印flask專案的配置
<Flask 'manual_push'>
<Config {'ENV': 'production', 'DEBUG': False, 'TESTING': False, 'PROPAGATE_EXCEPTIONS': None, 'PRESERVE_CONTEXT_ON_EXCEPTION': None, 'SECRET_KEY': None, 'PERMANENT_SESSION_LIFETIME': datetime.timedelta(days=31), 'USE_X_SENDFILE': False, 'SERVER_NAME': None, 'APPLICATION_ROOT': '/', 'SESSION_COOKIE_NAME': 'session', 'SESSION_COOKIE_DOMAIN': None, 'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_HTTPONLY': True, 'SESSION_COOKIE_SECURE': False, 'SESSION_COOKIE_SAMESITE': None, 'SESSION_REFRESH_EACH_REQUEST': True, 'MAX_CONTENT_LENGTH': None, 'SEND_FILE_MAX_AGE_DEFAULT': None, 'TRAP_BAD_REQUEST_ERRORS': None, 'TRAP_HTTP_EXCEPTIONS': False, 'EXPLAIN_TEMPLATE_LOADING': False, 'PREFERRED_URL_SCHEME': 'http', 'JSON_AS_ASCII': True, 'JSON_SORT_KEYS': True, 'JSONIFY_PRETTYPRINT_REGULAR': False, 'JSONIFY_MIMETYPE': 'application/json', 'TEMPLATES_AUTO_RELOAD': None, 'MAX_COOKIE_SIZE': 4093}>

請求上下文和程式上下文為什麼要分開

在flask0.1中請求上下文和程式上下文是在同一個棧中儲存,為什麼後面分離開來?
是為了非 web 應用的場合。所謂非web應用場合就是沒有請求到來時,也需要使用程式上下文的場景,典型代表就是flask shell。在處理一些資料庫匯入,指令碼等場景時沒有網路請求到來,也就沒有請求上下文和程式上下文。所以current_app不可使用,如配置資訊、orm資料庫等都不可使用。為了能夠在沒有請求到來的場景下使用程式上下文,所以將程式上下文和請求上下文分開,然後使用手動入棧程式上下文的方式來方便的使用flask提供的功能。也就是上面分析的手動入棧。

為什麼用棧來儲存上下文物件

學到這裡時我其實有一個疑問,為什麼請求上下文物件和程式上下文物件要用本地棧來儲存呢?用本地執行緒不可以嗎?

網上給出的解釋是flask通過中介軟體同時處理兩個app的程式,兩個app的請求會同時存在,用本地棧能夠讓各自請求找到自己的資料。但是根據flask的服務端模型,同一時間,一個執行緒只處理一個請求,根本不會有多個請求同時被處理的情況,用本地執行緒也可以儲存上下文,那麼到底什麼原因讓flask用本地棧這種資料結構呢?

Flask 處理模型

首先說明flask的服務處理模型
flask 有兩種啟動模式,分別是單執行緒和多執行緒。單執行緒啟動就是請求只在一個執行緒中處理,當上一個請求沒有返回,下一個請求需要等待;多執行緒請求中每一個請求到來不會被阻塞,會有多個執行緒提供處理。預設是多執行緒

單程式

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world(num):
    return f"Hello world!"

if __name__ == '__main__':
    app.run()

多程式

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world(num):
    return f"Hello world!"

if __name__ == '__main__':
    app.run(processes=True)

但是不管是多程式啟動還是單程式啟動,同一個執行緒同一個時刻只會處理一個請求, 又因為本地執行緒技術(上一篇有介紹本地執行緒技術Flask中請求資料的優雅傳遞)也就是說同一個時刻只有一個request物件,那麼為什麼flask儲存request物件時使用的是本地棧而不是本地執行緒呢?

這個問題的答案是:當程式上下文手動入棧時,可以入棧多個程式上下文。這樣會同時存在多個程式上下文,為了獲取正確的程式上下文,需要使用棧這種先進後出的結構。
如果沒有明白原因沒關係,可以從下面的程式中找到解釋。

import time
from flask import Flask, current_app

app1 = Flask('app01')
app2 = Flask('app02')

def do_something():
    print("app1 壓棧------------------------------------")
    with app1.app_context():
        time.sleep(5)
        print("app2 壓棧------------------------------------")
        with app2.app_context():
            pass # current_app是程式上下文,上一個棧頂元素是app1,當app2被推入時獲取的是app2
        print("app2 is 出棧-------------------------------------")
        # 當app2出棧之後,棧頂元素又變成app1,而這時獲取到的又是棧頂元素

do_something()

同時在Flask中程式上下文入棧之後列印了除錯資訊

當app1入棧後,_app_ctx_stack中只有一個元素,就是app1,這時訪問current_app就是app1;
當app2入棧後,_app_ctx_stack中有兩個元素,且棧頂是app2。這時訪問current_app就是app2;
當app2出棧後,_app_ctx_stack中剩餘一個元素,就是app1,這時再訪問current_app就是app1。
正是通過這種棧資料結構,讓處理函式都是獲取自己程式上下文中的current_app。

小結

程式上下文和請求上下文都是儲存在本地棧中,因為手動入棧時存在多個上下文環境巢狀,所以需要棧這樣的資料結構保持最新的上下文在最先獲得。

相關文章