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 學習資料


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM