Flask中本地棧的使用

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。

小結

程序上下文和請求上下文都是保存在本地棧中,因爲手動入棧時存在多個上下文環境嵌套,所以需要棧這樣的數據結構保持最新的上下文在最先獲得。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章