Flask 上下文機制和線程隔離

1. 計算機科學領域的任何問題均可以經過增長一個間接的中間層來解決, 上下文機制就是這句話的體現。html

2. 若是一次封裝解決不了問題,那就再來一次python

上下文:至關於一個容器,保存了Flask程序運行過程當中的一些信息 源碼:flask/ctx.pygit

  • 請求上下文:Flask從客戶端收到請求時,要讓視圖函數能訪問一些對象,這樣才能處理請求,要想讓視圖函數可以訪問請求對象,一個顯而易見的方式是將其做爲參數傳入視圖函數,不過這會致使程序中的每一個視圖函數都增長一個參數,除了訪問請求對象,若是視圖函數在處理請求時還要訪問其餘對象,狀況會變得更糟。爲了不大量無關緊要的參數把視圖函數弄得一團糟,Flask使用上下文臨時把某些對象變爲全局可訪問。這就是一種重構設計思路github

    • request 封裝了HTTP請求的內容,針對http請求,也是一種符合WSGI接口規範的設計(關於WSGI可參考我對該協議的理解和實現demo mini-wsgi-web),如 request.args.get('user')
    • session 用來記錄請求會話中的信息,針對的是用戶信息,如 session['name'] = user.id
  • 應用上下文:應用程序上下文,用於存儲應用程序中的變量web

    • current_app 存儲應用配置,數據庫鏈接等應用相關信息
    • g變量 做爲flask程序全局的一個臨時變量, 充當者中間媒介的做用,咱們能夠經過它傳遞一些數據,g保存的是當前請求的全局變量,不一樣的請求會有不一樣的全局變量,經過不一樣的thread id區別
# context locals
# 使用代理模式 LocalProxy
_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'))

1. working outside application context

#!/usr/bin/python3
# -*- coding: utf-8 -*-
# __author__ = '__JonPan__'

from flask import Flask, current_app

app = Flask(__name__)
a = current_app
# is_debug = current_app.config['DEBUG']

@app.route('/')
def index():
    return '<h1>Hello World. Have a nice day! </1>'


if __name__ == '__main__':
    app.run(host='localhost', port=8888)

報錯:sql

Exception has occurred: RuntimeError數據庫

Working outside of application context.編程

2. flask 上下文出入棧

flask上下文對象出入棧模型圖flask

在應用開發中可用直接引用current_app不會報錯,是由於當在一個請求中使用的時候,flask會判斷_app_ctx_stack棧頂是否有可用對象,若是沒有就會自動推入一個App. 咱們獲取的current_app就是獲取的棧頂元素設計模式

# flask/globals.py

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)

修改代碼:將app對象推入棧頂

# ...
# 將app_context 推入棧中
ctx = app.app_context()
ctx.push()
a = current_app

is_debug = current_app.config['DEBUG']
ctx.pop()
# ...

# 更有pythonnic的寫法
# 將app_context 推入棧中
# with app.app_context():
#     a = current_app
#     is_debug = current_app.config['DEBUG']

不在出現unbound狀態。可正常運行

既然flask會自動幫咱們檢測棧頂元素是否存在,爲何咱們還要作這一步操做,當咱們在寫離線應用,或者單元測試的時候就須要用到,由於請求是模擬的,不是在application context中的了。

3. python中的上文管理器

實現了__enter____exit__方法的對象就是一個上文管理器。

  1. 實現了__enter____exit__方法的對象就是一個上文管理器。
class MyResource:
    def __enter__(self):
        print('connect ro resource')
        return self
    
    def __exit__(self, exc_type, exc_value, tb):
        if tb:
            print('process exception')
        else:
            print('no exception')
        print('close resource connection')
        # return True
        # return False
    
    def query(self):
        print('query data')


try:
    with MyResource() as r:
        1/0
        r.query()
except Exception as e:
    print(e)

with MyResour ce() as r as 的別名r指向的不是上想問管理器對象,而是__enter__方法返回的值,在以上代碼確實是返回了對象自己。

__exit__ 方法 處理退出上下文管理器對象時的一些資源清理工做,並處理異常,三個參數

  • exc_type 異常類型
  • exc_value 異常緣由解釋
  • tb traceback

__exit__實際上是有返回值的,return True表示,異常信息已經在本方法中處理,外部可不接收異常,return False 表示將異常拋出給上層邏輯處理,默認不寫返回,即默認值是None, None也表示False

  1. 另外一種實現上下文管理器的方法是contextmanager 裝飾器

使用 contextmanager 的裝飾器,能夠簡化上下文管理器的實現方式。原理是經過 yield 將函數分割成兩部分,yield 以前的語句在__enter__ 方法中執行,yield 以後的語句在__exit__ 方法中執行。緊跟在 yield 後面的值是函數的返回值。

from contextlib import contextmanager


class MyResource:
    
    def query(self):
        print('query data')


@contextmanager
def my_resource():
    print('connect ro resource')
    yield MyResource()
    print('close resource connection')


try:
    with my_resource() as r:
        r.query()
except Exception as e:
    print(e)

兩種方法並無說哪種方法好,各有優略,看代碼環境的使用場景來決定。contextmanager

如下就是一個簡單的需求,給文字加書名號。

@contextmanager
def book_mark():
    print('《', end='')
    yield 
    print('》', end='')


with book_mark():
    print('你還年輕? peer已經年少有爲!', end='')

實際應用場景,封裝一些公用方法。

本來業務邏輯中是這樣的, 在全部的模型類中,在新建資源的時候都須要addcommit, 或者rollback的操做。

@login_required
def save_to_gifts(isbn):
    if current_user.can_save_to_list(isbn):
        try:
            gift = Gift()
            gift.isbn = isbn
            gift.uid = current_user.id
            db.session.add(gift)
            db.session.commit()
        except Exception as e:
            db.session.rollback()
            raise e

簡化後

@login_required
def save_to_gifts(isbn):
    if current_user.can_save_to_list(isbn):
        with db.auto_commit():
            gift = Gift()
            gift.isbn = isbn
            gift.uid = current_user.id
            db.session.add(gift)

其中數據庫的封裝以下

#!/usr/bin/python3
# -*- coding: utf-8 -*-
# __author__ = '__JonPan__'

from contextlib import contextmanager
from flask_sqlalchemy import SQLAlchemy as _SQLAlchemy, BaseQuery


# 抽離公用代碼,with 形式來完成db.commit  rollback
class SQLAlchemy(_SQLAlchemy):

    @contextmanager
    def auto_commit(self):
        try:
            yield
            self.session.commit()
        except Exception as e:
            self.session.rollback()
            raise e


class Query(BaseQuery):
    def filter_by(self, **kwargs):
        if 'status' not in kwargs.keys():
            kwargs['status'] = 1
        return super(Query, self).filter_by(**kwargs)


db = SQLAlchemy(query_class=Query)

線程隔離機制

如何實現一個Reqeust 指向多個請求實例,且要區分該實例對象所綁定的用戶?

字典:

request = {'key1': val1, 'key2': val2}

flask引用 werkzeug 中的 local.Local 實現線程隔離

Local

# werkzeug\local.py
class Local(object):
    __slots__ = ('__storage__', '__ident_func__')

    def __init__(self):
        object.__setattr__(self, '__storage__', {})
        object.__setattr__(self, '__ident_func__', get_ident)

    def __iter__(self):
        return iter(self.__storage__.items())

    def __call__(self, proxy):
        """Create a proxy for a name."""
        return LocalProxy(self, proxy)

    def __release_local__(self):
        self.__storage__.pop(self.__ident_func__(), None)

    def __getattr__(self, name):
        try:
            return self.__storage__[self.__ident_func__()][name]
        except KeyError:
            raise AttributeError(name)

    def __setattr__(self, name, value):
        # 獲取當前線程的id
        ident = self.__ident_func__()
        storage = self.__storage__
        try:
            storage[ident][name] = value
        except KeyError:
            storage[ident] = {name: value}

    def __delattr__(self, name):
        try:
            del self.__storage__[self.__ident_func__()][name]
        except KeyError:
            raise AttributeError(name)

能夠看到是使用線程ID來綁定不一樣的上線文對象。

LocalStack

class LocalStack(object):

    """This class works similar to a :class:`Local` but keeps a stack
    of objects instead.  This is best explained with an example::

        >>> ls = LocalStack()
        >>> ls.push(42)
        >>> ls.top
        42
        >>> ls.push(23)
        >>> ls.top
        23
        >>> ls.pop()
        23
        >>> ls.top
        42

    They can be force released by using a :class:`LocalManager` or with
    the :func:`release_local` function but the correct way is to pop the
    item from the stack after using.  When the stack is empty it will
    no longer be bound to the current context (and as such released).

    By calling the stack without arguments it returns a proxy that resolves to
    the topmost item on the stack.

    .. versionadded:: 0.6.1
    """

    def __init__(self):
        self._local = Local()

    def __release_local__(self):
        self._local.__release_local__()

    def _get__ident_func__(self):
        return self._local.__ident_func__

    def _set__ident_func__(self, value):
        object.__setattr__(self._local, '__ident_func__', value)
    __ident_func__ = property(_get__ident_func__, _set__ident_func__)
    del _get__ident_func__, _set__ident_func__

    def __call__(self):
        def _lookup():
            rv = self.top
            if rv is None:
                raise RuntimeError('object unbound')
            return rv
        return LocalProxy(_lookup)

    def push(self, obj):
        """Pushes a new item to the stack"""
        rv = getattr(self._local, 'stack', None)
        if rv is None:
            self._local.stack = rv = []
        rv.append(obj)
        return rv

    def pop(self):
        """Removes the topmost item from the stack, will return the
        old value or `None` if the stack was already empty.
        """
        stack = getattr(self._local, 'stack', None)
        if stack is None:
            return None
        elif len(stack) == 1:
            release_local(self._local)
            return stack[-1]
        else:
            return stack.pop()

    @property
    def top(self):
        """The topmost item on the stack.  If the stack is empty,
        `None` is returned.
        """
        try:
            return self._local.stack[-1]
        except (AttributeError, IndexError):
            return None

Local使用字典的方式實現線程隔離,LocalStack 則封裝Local實現了線程隔離的棧結構

總結

  1. flask上下文的實現使用了設計模式中的代理模式,current_app, requsts, 等代理對象都是線程隔離的, 當咱們啓動一個線程去執行一個異步操做須要用到應用上下文時(需傳入app對象), 若是傳入current_app, 此時的app unbond的狀態, 因爲線程id改變了, 因此在新的線程中 全部的棧都是空的, 可是在整個web中 由Flask 實例化的 app是惟一的, 因此獲取app傳入是能夠的 app = current_app._get_current_object()
  2. 線程隔離的實現機制就是利用線程ID+字典, 使用線程隔離的意義在於:使當前線程可以正確引用到他本身所建立的對象,而不是引用到其餘線程所建立的對象
  3. current_app -> (LocalStack.top = AppContext top.app = Flask)
  4. request -> (LocalStack.top = RequestContext.top.request = Request)

參考資料

Python Flask高級編程-七月

相關文章
相關標籤/搜索