Flask 中的 Context 初探

Zheaoli發表於2018-02-21

Flask 中的 Context 初探

大家新年好!鑑於今年春晚非常好看,我覺得承受不起,於是來寫點辣雞水文娛樂下大家,這也是之前立的若干 Flag 中的一個

正文

做過 Flask 開發的朋友都知道 Flask 中存在著兩個概念,一個叫 App Context , 一個叫 Request Context 。 這兩個算是 Flask 中很獨特的一種機制。

從一個 Flask App 讀入配置並啟動開始,就進入了 App Context,在其中我們可以訪問配置檔案、開啟資原始檔、通過路由規則反向構造 URL。當 WSGI Middleware 呼叫 Flask App 的時候開始,就進入了 Request Context 。我們可以獲取到其中的 HTTP HEADER 等操作,同時也可以進行 SESSION 等操作。

不過作為辣雞選手而言,經常分不清為什麼會存在這兩個 Context ,沒事,我們慢慢來說一說。

預備知識

首先要清楚一點,我們要在同一個程式中隔離不同執行緒的資料,那麼我們會優先選擇 threading.local ,來實現資料彼此隔離的需求。但是現在有個問題來了,現在我們併發模型可能並不是只有傳統意義上的程式-執行緒模型。也有可能是 coroutine(協程) 模型。常見的就是 Greenlet/Eventlet 。在這種情況下,threading.local 就沒法很好的滿足我們的需求。於是 Werkzeug 實現了自己的 Local 即 werkzeug.local.Local

那麼 Werkzeug 自己實現的 Local 和標準的 threading.local 相比有什麼不同呢?我們記住最大的不同點在於

前者會在 Greenlet 可用的情況下優先使用 Greenlet 的 ID 而不是執行緒 ID 以支援 Gevent 或 Eventlet 的排程,後者只支援多執行緒排程;

Werkzeug 另外還實現了兩種資料結構,一個叫 LocalStack ,一個叫做 LocalProxy

LocalStack 是基於 Local 實現的一個棧結構。棧的特性就是後入先出。當我們進入一個 Context 時,將當前的的物件推入棧中。然後我們也可以獲取到棧頂元素。從而獲取到當前的上下文資訊。

LocalProxy 是代理模式的一種實現。在例項化的時候,傳入一個 callable 的引數。然後這個引數被呼叫後將會返回一個 Local 物件。我們後續的所有操作,比如屬性呼叫,數值計算等,都會轉發到這個引數返回的 Local 物件上。

現在大家可能不太清楚,我們為什麼要用 LocalProxy 來進行操作,我們來給大家看一個例子


from werkzeug.local import LocalStack
test_stack = LocalStack()
test_stack.push({'abc': '123'})
test_stack.push({'abc': '1234'})

def get_item():
    return test_stack.pop()

item = get_item()

print(item['abc'])
print(item['abc'])

複製程式碼

你看我們這裡的輸出的的值,都是統一的 1234 ,但是我們這裡想做到的是每次獲取的值都是棧頂的最新的元素,那麼我們這個時候就應該用 proxy 模式了

from werkzeug.local import LocalStack, LocalProxy
test_stack = LocalStack()
test_stack.push({'abc': '123'})
test_stack.push({'abc': '1234'})

def get_item():
    return test_stack.pop()

item = LocalProxy(get_item)

print(item['abc'])
print(item['abc'])

複製程式碼

你看我們這裡就是 Proxy 的妙用。

Context

由於 Flask 基於 Werkzeug 實現,因此 App Context 以及 Request Context 是基於前文中所說的 LocalStack 實現。

從命名上,大家應該可以看出,App Context 是代表應用上下文,可能包含各種配置資訊,比如日誌配置,資料庫配置等。而 Request Context 代表一個請求上下文,我們可以獲取到當前請求中的各種資訊。比如 body 攜帶的資訊。

這兩個上下文的定義是在 flask.ctx 檔案中,分別是 AppContext 以及 RequestContext 。而構建上下文的操作則是將其推入在 flask.globals 檔案中定義的 _app_ctx_stack 以及 _request_ctx_stack 中。前面說了 LocalStack 是“執行緒”(這裡可能是傳統意義上的執行緒,也有可能是 Greenlet 這種)隔離的。同時 Flask 每個執行緒只處理一個請求,因此可以做到請求隔離。

app = Flask(__name__) 構造出一個 Flask App 時,App Context 並不會被自動推入 Stack 中。所以此時 Local Stack 的棧頂是空的,current_app 也是 unbound 狀態。


from flask import Flask

from flask.globals import _app_ctx_stack, _request_ctx_stack

app = Flask(__name__)

_app_ctx_stack.top
_request_ctx_stack.top
_app_ctx_stack()
# <LocalProxy unbound>
from flask import current_app
current_app
# <LocalProxy unbound>
複製程式碼

作為 web 時,當請求進來時,我們開始進行上下文的相關操作。整個流程如下:

image

好了現在有點問題:

  1. 為什麼要區分 App Context 以及 Request Context

  2. 為什麼要用棧結構來實現 Context ?

很久之前看過的松鼠奧利奧老師的博文Flask 的 Context 機制 解答了這個問題

這兩個做法給予我們 多個 Flask App 共存 和 非 Web Runtime 中靈活控制 Context 的可能性。

我們知道對一個 Flask App 呼叫 app.run() 之後,程式就進入阻塞模式並開始監聽請求。此時是不可能再讓另一個 Flask App 在主執行緒執行起來的。那麼還有哪些場景需要多個 Flask App 共存呢?前面提到了,一個 Flask App 例項就是一個 WSGI Application,那麼 WSGI Middleware 是允許使用組合模式的,比如:


from werkzeug.wsgi import DispatcherMiddleware
from biubiu.app import create_app
from biubiu.admin.app import create_app as create_admin_app

application = DispatcherMiddleware(create_app(), {
    '/admin': create_admin_app()
})

複製程式碼

奧利奧老師文中舉了一個這樣一個例子,Werkzeug 內建的 Middleware 將兩個 Flask App 組合成一個一個 WSGI Application。這種情況下兩個 App 都同時在執行,只是根據 URL 的不同而將請求分發到不同的 App 上處理。

但是現在很多朋友有個問題,就是為什麼這裡不用 Blueprint ?

  • Blueprint 是在同一個 App 下執行。其掛在 App Context 上的相關資訊都是一致的。但是如果要隔離彼此的資訊的話,那麼用 App Context 進行隔離,會比我們用變數名什麼的隔離更為方便

  • Middleware 模式是 WSGI 中允許的特性,換句話來講,我們將 Flask 和另外一個遵循 WSGI 協議的 web Framework (比如 Django)那麼也是可行的。

但是 Flask 的兩種 Context 分離更大的意義是為了非 web 應用的場合。Flask 官方文件中有這樣一段話

The main reason for the application’s context existence is that in the past a bunch of functionality was attached to the request context for lack of a better solution. Since one of the pillars of Flask’s design is that you can have more than one application in the same Python process.

這句話換句話說 App Context 存在的意義是針對一個程式中有多個 Flask App 場景,這樣場景最常見的就是我們用 Flask 來做一些離線指令碼的程式碼。

好了,我們來聊聊 Flask 非 Web 應用的場景

比如,我們有個外掛叫 Flask-SQLAlchemy 然後這裡有個使用場景 首先我們現在有這樣一個程式碼

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

database = Flask(__name__)
database.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
db = SQLAlchemy(database)


class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)

    def __repr__(self):
        return '<User %r>' % self.username
複製程式碼

這裡你應該注意到最開始的幾個關鍵點,第一個,就是 database.config ,是的沒錯,Flask-SQLAlchemy 就是從當前的 app 中獲取到對應的 config 資訊來建立資料庫連結。那麼傳遞 app 的方式有兩種,第一種,就是直接如上圖一樣,直接 db = SQLAlchemy(database) ,這個很容易理解,第二種,如果我們不傳的話,那麼 Flask-SQLAlchemy 中通過 current_app 來獲取當前的 app 然後獲取對應的 config 建立連結。 那麼問題來了,為什麼會存在第二種這種方法呢

給個場景吧,現在我兩個資料庫配置不同的 app 共用一個 Model 那麼應該怎麼做?其實很簡單

首先寫 一個 model 檔案,比如就叫 data/user_model.py 吧

from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()


class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)

    def __repr__(self):
        return '<User %r>' % self.username
複製程式碼

好了,那麼在我們的應用檔案中,我們便可以這樣寫

from data.user_model import User
database = Flask(__name__)
database.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
with database.app_context():
    db.init_app(current_app)
    db.create_all()
    admin = User(username='admin', email='admin@example.com')
    db.session.add(admin)
    db.session.commit()
    print(User.query.filter_by(username="admin").first())

database1 = Flask(__name__)
database1.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test1.db'
with database1.app_context():
    db.init_app(current_app)
    db.create_all()
    admin = User(username='admin_test', email='admin@example.com')
    db.session.add(admin)
    db.session.commit()
    print(User.query.filter_by(username="admin").first())
複製程式碼

你看這樣是不是就好懂了一些,通過 app context ,我們 Flask-SQLAlchemy 可以通過 current_app 來獲取當前 app ,繼而獲取相關的 config 資訊

這個例子還不夠妥當,我們現在再來換一個例子

from flask import Flask, current_app
import logging

app = Flask("app1")
app2 = Flask("app2")

app.config.logger = logging.getLogger("app1.logger")
app2.config.logger = logging.getLogger("app2.logger")

app.logger.addHandler(logging.FileHandler("app_log.txt"))
app2.logger.addHandler(logging.FileHandler("app2_log.txt"))

with app.app_context():
    with app2.app_context():
        try:
            raise ValueError("app2 error")
        except Exception as e:
            current_app.config.logger.exception(e)
    try:
        raise ValueError("app1 error")
    except Exception as e:
        current_app.config.logger.exception(e)
複製程式碼

好了,這段程式碼很清晰了,含義很清晰,就是通過獲取當前上下文中的 app 中的 logger 來輸出日誌。同時這段程式碼也很清晰的說明了,我們為什麼要用棧這樣一種資料結構來維護上下文。

首先看一下 app_context() 的原始碼


    def app_context(self):
        """Binds the application only.  For as long as the application is bound
        to the current context the :data:`flask.current_app` points to that
        application.  An application context is automatically created when a
        request context is pushed if necessary.

        Example usage::

            with app.app_context():
                ...

        .. versionadded:: 0.9
        """
        return AppContext(self)
複製程式碼

嗯,很簡單,只是構建一個 AppContext 物件返回,然後我們看看相關的程式碼


class AppContext(object):
    """The application context binds an application object implicitly
    to the current thread or greenlet, similar to how the
    :class:`RequestContext` binds request information.  The application
    context is also implicitly created if a request context is created
    but the application is not on top of the individual application
    context.
    """

    def __init__(self, app):
        self.app = app
        self.url_adapter = app.create_url_adapter(None)
        self.g = app.app_ctx_globals_class()

        # Like request context, app contexts can be pushed multiple times
        # but there a basic "refcount" is enough to track them.
        self._refcnt = 0

    def push(self):
        """Binds the app context to the current context."""
        self._refcnt += 1
        if hasattr(sys, 'exc_clear'):
            sys.exc_clear()
        _app_ctx_stack.push(self)
        appcontext_pushed.send(self.app)

    def pop(self, exc=_sentinel):
        """Pops the app context."""
        try:
            self._refcnt -= 1
            if self._refcnt <= 0:
                if exc is _sentinel:
                    exc = sys.exc_info()[1]
                self.app.do_teardown_appcontext(exc)
        finally:
            rv = _app_ctx_stack.pop()
        assert rv is self, 'Popped wrong app context.  (%r instead of %r)' \
            % (rv, self)
        appcontext_popped.send(self.app)

    def __enter__(self):
        self.push()
        return self

    def __exit__(self, exc_type, exc_value, tb):
        self.pop(exc_value)

        if BROKEN_PYPY_CTXMGR_EXIT and exc_type is not None:
            reraise(exc_type, exc_value, tb)
複製程式碼

emmmm,首先 push 方法就是將自己推入 _app_ctx_stack ,而 pop 方法則是將自己從棧頂推出。然後我們看到兩個方法含義就很明確了,在進入上下文管理器的時候,將自己推入棧,然後退出上下文管理器的時候,將自己推出。

我們都知道棧的一個性質就是,後入先出,棧頂的永遠是最新插入進去的元素。而看一下我們 current_app 的原始碼


def _find_app():
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return top.app
    
current_app = LocalProxy(_find_app)

複製程式碼

嗯,很明瞭了,就是獲取當前棧頂的元素,然後進行相關操作。

嗯,通過這樣對於棧的不斷操作,就能讓 current_app 獲取到元素是我們當前上下文中的 app 。

額外的講解: g

g 也是我們常用的幾個全域性變數之一。在最開始這個變數是掛載在 Request Context 下的。但是在 0.10 以後,g 就是掛載在 App Context 下的。可能有同學不太清楚為什麼要這麼做。

首先,說一下 g 用來幹什麼

官方在上下文這一張裡有這一段說明

The application context is created and destroyed as necessary. It never moves between threads and it will not be shared between requests. As such it is the perfect place to store database connection information and other things. The internal stack object is called flask._app_ctx_stack. Extensions are free to store additional information on the topmost level, assuming they pick a sufficiently unique name and should put their information there, instead of on the flask.g object which is reserved for user code.

大意就是說,資料庫配置和其餘的重要配置資訊,就掛載 App 物件上。但是如果是一些使用者程式碼,比如你不想一層層函式傳資料的話,然後有一些變數需要傳遞,那麼可以掛在 g 上。

同時前面說了,Flask 並不僅僅可以當做一個 Web Framework 使用,同時也可以用於一些非 web 的場合下。在這種情況下,如果 g 是屬於 Request Context 的話,那麼我們要使用 g 的話,那麼就需要手動構建一個請求,這無疑是不合理的。

最後

大年三十寫這篇文章,現在發出來,我的辣雞也是無人可救了。Flask 的上下文機制是其最重要的特性之一。通過合理的利用上下文機制,我們可以再更多的場合下去更好的利用 flask 。嗯,本次的辣雞文章寫作活動就到此結束吧。希望大家不會扔我臭雞蛋!然後新年快樂!

相關文章