Flask 中的 Context 初探

Flask 中的 Context 初探

你們新年好!鑑於今年春晚很是好看,我以爲承受不起,因而來寫點辣雞水文娛樂下你們,這也是以前立的若干 Flag 中的一個web

正文

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

從一個 Flask App 讀入配置並啓動開始,就進入了 App Context,在其中咱們能夠訪問配置文件、打開資源文件、經過路由規則反向構造 URL。當 WSGI Middleware 調用 Flask App 的時候開始,就進入了 Request Context 。咱們能夠獲取到其中的 HTTP HEADER 等操做,同時也能夠進行 SESSION 等操做。數據庫

不過做爲辣雞選手而言,常常分不清爲何會存在這兩個 Context ,沒事,咱們慢慢來講一說。flask

預備知識

首先要清楚一點,咱們要在同一個進程中隔離不一樣線程的數據,那麼咱們會優先選擇 threading.local ,來實現數據彼此隔離的需求。可是如今有個問題來了,如今咱們併發模型可能並非只有傳統意義上的進程-線程模型。也有多是 coroutine(協程) 模型。常見的就是 Greenlet/Eventlet 。在這種狀況下,threading.local 就無法很好的知足咱們的需求。因而 Werkzeug 實現了本身的 Local 即 werkzeug.local.Localsession

那麼 Werkzeug 本身實現的 Local 和標準的 threading.local 相比有什麼不一樣呢?咱們記住最大的不一樣點在於數據結構

前者會在 Greenlet 可用的狀況下優先使用 Greenlet 的 ID 而不是線程 ID 以支持 Gevent 或 Eventlet 的調度,後者只支持多線程調度;多線程

Werkzeug 另外還實現了兩種數據結構,一個叫 LocalStack ,一個叫作 LocalProxy併發

LocalStack 是基於 Local 實現的一個棧結構。棧的特性就是後入先出。當咱們進入一個 Context 時,將當前的的對象推入棧中。而後咱們也能夠獲取到棧頂元素。從而獲取到當前的上下文信息。app

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 。嗯,本次的辣雞文章寫做活動就到此結束吧。但願你們不會扔我臭雞蛋!而後新年快樂!

相關文章
相關標籤/搜索