Flask0.1原始碼(1)-五個全域性物件

Dxtr發表於2018-11-20

flask-0.1 原始碼中有五個全域性物件, 分別是: _request_ctx_stack, current_app, request, sessiong. 從原始碼可以看出, 其餘四個都會依賴於 _request_ctx_stack. 單從命名上來看, 這應該是一個用於存放請求上下文的棧.

# flask-0.1 中不區分 app_ctx 和 request_ctx

# 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)
複製程式碼

這就讓我非常不解了.

不解

我們先拋開具體棧的實現, 僅僅當它是一個棧來考慮, 並且忽略多執行緒. 只有一個執行緒的情況下, Flask 同一時間只能處理一個請求, 那麼只需要一個請求上下文例項也可以達到同樣的效果, 為什麼會用棧來實現呢?
Stack Overflow 上有一個小哥和我一樣不解, 問題下面的回答非常精彩(建議回答和評論都仔細看一下).

首先回答我不解的是 Flask 中的 url_forredrict. Flask 是支援在內部直接重定向的, 用棧來存放請求上下文可以很方便地支援這一特性, 但是如果僅僅是用一個請求上下文例項而不是棧的話卻是很難實現的. 舉個?:
如果 A 請求過來, 你需要在處理 A 請求的時候重定向到 B, 這個時候, 如果是棧的話, 當前請求就會被"掛起", B 請求會被壓入棧中, 等待 B 請求處理完, 從棧中 pop 出來之後, 就可以繼續處理 A 請求, 或者將 B 請求的結果作為 A 請求的結果返回; 但是如果只是一個例項的話, A 請求上下文就會被 B 替換, 就會造成 A 請求無法返回.


搞清楚為什麼用棧來儲存請求上下文之後, 我們再來棧的具體實現.

要搞清楚這是一個什麼棧, 就得引入多執行緒了. 如果我們以多個執行緒啟動 Flask, 那便可以同時處理多個請求, 但是 Flask 例項只有一個, 那它又是如何處理請求上下文的呢? 它是怎麼做到不同執行緒上請求上下文分離的呢?

仔細觀察上面的原始碼部分, 可以發現 Flask 的全域性物件 _request_ctx_stack 是 Werkzeug LocalStack 類的一個例項. 可見, 不同執行緒請求上下文分離的奧祕應該隱藏在 LocalStack 這個類中.
為了理解 LocalStack,我們先引入 Werkzeug 的另外一個類 --- Local.
簡單來說, Local 實現的功能是同一個例項在不同執行緒(Werkzeug支援執行緒和 greenlet)下變數的分離. 比如下面的例子中, Local 的例項是全域性的, 但是在不同的執行緒(greenlet)下, 例項中變數的值可以是不同的. 當你線上程 1 中訪問 local.name 的時候, 返回的值是 John, 但是線上程 2 中返回的卻是 'Debbie':

local = Local()

# ...

# on thread 1
local.name = 'John'

# ...

# on thread 2
local.name = 'Debbie'
複製程式碼

那這是怎麼做到的呢? 簡化版的 Local 實現如下:

"""
Local 原始碼:  
https://github.com/pallets/werkzeug/blob/5d80fa2cd1008abaa9450b40a44f0df90a842489/werkzeug/local.py#L51
"""

# 簡化版 Local 實現
class Local:
    def __init__(self)
        self.storage = {}
    
    def __getattr__(self, name):
        context_id = get_ident()  # we get the current thread's or greenlet's id
        contextual_storage = self.storage.setdefault(context_id, {})
        try:
            return contextual_storage[name]
        except KeyError:
            raise AttributeError('name')
    
    def __setattr__(self, name, value):
        context_id = get_ident()
        contextual_storage = self.storage.setdefault(context_id, {})
        contextual_storage[name] = value
    
    
    def __release_local__(self):
        context_id = get_ident()
        self.storage.pop(context_id, None)
        
local = Local()
複製程式碼

程式碼中 get_ident() 起著至關重要的作用, 它能夠識別出當前所在的執行緒, 並且將當前所線上程的上下文變數儲存在例項字典中, 以執行緒 id 作為 key, 從而做到了不同執行緒下上下文變數的分離.

理解 Local 後, 繼續看 Flask 中用來實現請求棧的 LocalStack. 先看看看簡化版的 LocalStack 實現:

"""
LocalStack 原始碼:  
https://github.com/pallets/werkzeug/blob/5d80fa2cd1008abaa9450b40a44f0df90a842489/werkzeug/local.py#L89
"""

# 簡化版 LocalStack
class LocalStack:
    def __init__(self):
        self.local = Local()
    
    def push(self, obj):
        """
        將新元素 push 到棧中
        """
        stack = getattr(self.local, 'stack', None)
        if stack is None:
            stack = []
            self.local.stack = stack
        stack.append(obj)
        return stack
    
    def pop(self):
        """
        pop 出棧頂元素
        如果棧為空, 返回 None
        """
        stack = getattr(self.local, 'stack', None)
        if stack is None:
            return None
        elif len(stack) = 1:
            release_local(self.local) # this simply releases the local
            return stack[-1]
        else:
            return tack.pop()
    
    @property
    def top(self):
        """
        獲取站頂元素, 不出棧
        若棧為空, 同樣返回 None
        """
        try:
            return self.local.stack[-1]
        except (AttributeError, IndexError):
            return None
複製程式碼

LocalStackLocal 包了一層(在 Local 中儲存了一個棧), 所以依然支援不同執行緒變數分離. 每個執行緒中都維護了一個請求棧, 且各個棧互不干擾. 並且實現了 push, poptop 方法, 分別對應進棧, 出棧和獲取棧頂元素三中常見棧操作.


另外, 其實 Flask 的這幾個全域性變數還是和 Local, LocalStack 不太一樣, 它是用 LocalProxy 實現的. 那 LocalProxy 又是什麼?
講真, 這個困惑了我很久.
首先還是解釋一下 LocalProxy 的作用吧. 就如它命名所示, 它起到的就是一個代理的作用.
就拿上面文中的 request 舉例來說, LocalProxy 的作用就是把所有對 request 的操作全部代理到請求棧頂元素(_request_ctx_stack.top.request)中.

"""
LocalProxy 原始碼:  
https://github.com/pallets/werkzeug/blob/5d80fa2cd1008abaa9450b40a44f0df90a842489/werkzeug/local.py#L254
"""

# 簡化版 LocalProxy
class LocalProxy(object):
    def __init__(self, local, name):
        # local 是一個 Local 例項
        # 或者是可回撥的, 通過它可以獲取到被代理的物件
        self.local = local
        # `name` 是被代理的物件的名字(key)
        self.name = name

    def _get_current_object(self):
        # 如果 local 是一個 Local 例項, 則他會有 `__release_local__` (被用於釋放 local 物件)
        if hasattr(self.local, '__release_local__'):
            try:
                return getattr(self.local, self.name)
            except AttributeError:
                raise RuntimeError('no object bound to %s' % self.name)

        # 如果不是 Local 例項, 就必須可以通過直接呼叫它獲取到被代理的物件
        return self.local(self.name)

    # 以下所有的魔術方法都被重寫
    # 使得對 LocalProxy 例項的所有操作都會被代理到被代理的物件
    @property
    def __dict__(self):
        try:
            return self._get_current_object().__dict__
        except RuntimeError:
            raise AttributeError('__dict__')

    def __repr__(self):
        try:
            return repr(self._get_current_object())
        except RuntimeError:
            return '<%s unbound>' % self.__class__.__name__

    def __bool__(self):
        try:
            return bool(self._get_current_object())
        except RuntimeError:
            return False

    # ... etc etc ... 

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

    def __setitem__(self, key, value):
        self._get_current_object()[key] = value

    def __delitem__(self, key):
        del self._get_current_object()[key]

    # ... and so on ...

    __setattr__ = lambda x, n, v: setattr(x._get_current_object(), n, v)
    __delattr__ = lambda x, n: delattr(x._get_current_object(), n)
    __str__ = lambda x: str(x._get_current_object())
    __lt__ = lambda x, o: x._get_current_object() < o
    __le__ = lambda x, o: x._get_current_object() <= o
    __eq__ = lambda x, o: x._get_current_object() == o

    # ... and so forth ...
複製程式碼

LocalProxy 重寫了很多魔術方法, 使得所有對其例項的操作全部轉移到被代理的物件上.

參考:
What is the purpose of Flask's context stacks?
werkzeug.local
flask 原始碼解析:上下文

相關文章