Flask進擊篇(2)——上下文管理

本文首發微信公衆號:戰渣渣
歡迎大家關注。

關聯知識

WEB開發——Python WSGI協議詳解
Flask進擊篇(1)——Flask運行流程

背景

在Flask中可直接導入from flask import request, current_app, g並直接使用,那Flask是如何保證這個request對象就是這個請求對象呢?

Flask官方中有提到使用的是本地線程對象,這篇文章就來揭示其原理

Flask線程間上下文安全

Falsk完成線程安全的原理,是在啓動之後進程裏維護request棧和app棧,棧是通過線程ID來保證每個請求的線程安全。

實現主要依賴三個類Local,LocalStack和LocalProxy,下面看一下具體的實現原理

三個類構建本地數據

1. Local

先看Local的源碼,實質並不是Flask中定義的,而是Flask依賴的werkzeug庫所定義。

# werkzeug\local.py

# get_ident獲取線程和協程的唯一標識

try:
    from greenlet import getcurrent as get_ident
except ImportError:
    try:
        from thread import get_ident
    except ImportError:
        from _thread import get_ident

# 實際的Local類
class Local(object):
    __slots__ = ('__storage__', '__ident_func__')

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

    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):
        ident = self.__ident_func__()
        storage = self.__storage__
        try:
            storage[ident][name] = value
        except KeyError:
            storage[ident] = {name: value}

可以看到其定義的兩個屬性__storage__, __ident_func__以及三個方法__getattr__,__setattr__,__release_local__。

  1. 屬性__storage__是多層級字典,第一層key是隱含的線程ID或者協程ID,第二層的key是實際使用的關鍵字

  2. 屬性__ident_func__可以看到是get_ident函數,get_ident函數要麼是通過thread庫獲取當前執行單元的線程ID,要麼是通過greenlet庫獲取當前執行協程的協程ID。

另外可以看到Local這個類的三個方法,實質是通過重寫Python內置函數__setattr__和__getattr__來實現線程或者協程間數據隔離

  1. 獲取local某屬性時

    如:local.age實質觸發的是__getattr__方法

    1. 先獲取到當前線程ID——__ident_func__函數獲取,然後在__storage__字典中找到線程ident對應的結果集

    2. 從獲取到的結果中再查找age屬性

  2. 設置local某屬性時

    如:local.age = 12 實際觸發的是__setattr__方法

    先獲取到當前線程ID——__ident_func__函數獲取,然後在__storage__字典設置相應的屬性字典集

另一個__release_local__方法就是將相應的線程數據刪除。

畫個簡圖比較起來更直觀一些。

在這裏插入圖片描述
主線程中生成一個對象local=Local(),三個線程中進行相同的操作local.no=每個線程對應的數。爲每個線程都開闢一個存儲,所以誰來取或者存就找到自己對應中的位置,雖然取得key都一樣,但是每次存取都是隻關於自己的值。

2. LocalStack

LocalStack也是定義在Flask所依賴的werkzeug庫,從字面意思來理解,它就是Local的堆棧操作,看一下源碼如何定義。

class LocalStack(object):
    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):
        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):
        try:
            return self._local.stack[-1]
        except (AttributeError, IndexError):
            return None

LocalStack實質就是圍繞着Local來進行操作,根據上面我們讀完Local的源碼可以看到,

  1. LocalStack定義了一個Local對象
  2. 給這個對象設置了一個stack屬性,且這個屬性是一個列表
  3. LocalStack中定義了對這個列表進行壓棧,出棧等方法
  4. 給類中的Local對象提供了自定義ident_func的方法

3. LocalProxy

LocalProxy字面意思就是做一個Local的代理,我們先從一個request的定義來看LocalProxy的用法,然後結合源碼來看LocalProxy到底是用來做什麼?

# venv/Lib/site-packages/werkzeug/local.py

@implements_bool
class LocalProxy(object):

	__slots__ = ("__local", "__dict__", "__name__", "__wrapped__")

    def __init__(self, local, name=None):
        object.__setattr__(self, "_LocalProxy__local", local)
        object.__setattr__(self, "__name__", name)
        if callable(local) and not hasattr(local, "__release_local__"):
            # "local" is a callable that is not an instance of Local or
            # LocalManager: mark it as a wrapped function.
            object.__setattr__(self, "__wrapped__", local)

    def _get_current_object(self):
        if not hasattr(self.__local, "__release_local__"):
            return self.__local()
        try:
            return getattr(self.__local, self.__name__)
        except AttributeError:
            raise RuntimeError("no object bound to %s" % self.__name__)

    def __getattr__(self, name):
        if name == "__members__":
            return dir(self._get_current_object())
        return getattr(self._get_current_object(), name)


    __setattr__ = lambda x, n, v: setattr(x._get_current_object(), n, v)

類中稍微有些難理解的就是關於object.__setattr__(self, “_LocalProxy__local”, local)的作用,實際就是給self設置一個__local屬性。這是Python類中關於私有變量的定義。可以看Python的官方定義python私有變量

可以看到這個類將所有Python類所內置的方法都進行重寫,重寫後所有的操作都是基於類中所定義的_get_current_object方法返回的對象進行操作。

而這個方法中返回值就是初始化時所給定的local對象執行返回的結果。如果創建時指定的不是Local對象,則直接執行此方法。如果給定的是Local對象,則根據類名查找對應的對象。

現在這個比較抽象,這個代理到底是做的什麼? 我們結合Flask定義全局的request對象來看。假如我們想獲取請求的方法是什麼,那我們使用的就是request.method。

下面是request定義的源碼

# flask/globals.py

def _lookup_req_object(name):
    top = _request_ctx_stack.top
    if top is None:
        raise RuntimeError(_request_ctx_err_msg)
    return getattr(top, name)

request = LocalProxy(partial(_lookup_req_object, "request"))
  1. 根據LocalProxy的源碼中重寫的__getattr__方法,先執行_get_current_object方法獲取到對象,然後再獲取返回對象method屬性。​
  2. 創建LocalProxy時傳遞的函數是_lookup_req_object的偏函數,實際就是_lookup_req_object且name=request
  3. 再LocalProxy中__local就是一個函數,所以在執行_get_current_object就是執行_lookup_req_object且name=request返回的值,然後再取其method屬性
  4. 此時再執行_lookup_req_object函數,從_request_ctx_stack獲取top的request

使用Proxy可以簡單快捷的使用request.method獲取相應的值,其核心就是每次獲取時都會執行對應的函數,而函數中每次返回的值都是線程安全。保證數據正確且優雅。 否則我們每次都去執行一個函數來獲取其值,然後再取其屬性。

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