Werkzeug(Flask)之Local、LocalStack和LocalProxy
在我們使用Flask以及Werkzeug框架的過程中,經常會遇到如下三個概念:Local、LocalStack和LocalProxy。尤其在學習Flask的Request Context和App Context的過程中,這幾個概念出現的更加頻繁,另外很多Flask插件都會使用這三個概念對應的技術。那麼這三個東西到底是什麼?我們為什麼需要它們?以及如何使用呢?本篇文章主要就是來解答這些問題。
Local
這部分我們重點介紹Local概念,主要分為以下幾個部分:
- 為什麼需要Local?
- Local的使用
- Local的實現
為什麼需要Local?
在Python的標準庫中提供了thread local
對象用於存儲thread-safe和thread-specific的數據,通過這種方式存儲的數據只在本線程中有效,而對於其它線程則不可見。正是基於這樣的特性,我們可以把針對線程全局的數據存儲進thread local
對象,舉個簡單的例子
>>from threading import local>>thread_local_data = local()>>thread_local_data.user_name="Jim">>thread_local_data.user_nameJim
使用thread local
對象雖然可以基於線程存儲全局變數,但是在Web應用中可能會存在如下問題:
- 有些應用使用的是greenlet協程,這種情況下無法保證協程之間數據的隔離,因為不同的協程可以在同一個線程當中。
- 即使使用的是線程,WSGI應用也無法保證每個http請求使用的都是不同的線程,因為後一個http請求可能使用的是之前的http請求的線程,這樣的話存儲於
thread local
中的數據可能是之前殘留的數據。
為了解決上述問題,Werkzeug開發了自己的local對象,這也是為什麼我們需要Werkzeug的local對象
Local的使用
先舉一個簡單的示例:
from werkzeug.local import Local, LocalManagerlocal = Local()local_manager = LocalManager([local])def application(environ, start_response): local.request = request = Request(environ) ...# make_middleware會確保當request結束時,所有存儲於local中的對象的reference被清除application = local_manager.make_middleware(application)
- 首先Local對象需要通過LocalManager來管理,初次生成LocalManager對象需要傳一個list類型的參數,list中是Local對象,當有新的Local對象時,可以通過
local_manager.locals.append()
來添加。而當LocalManager對象清理的時候會將所有存儲於locals中的當前context的數據都清理掉 - 上例中當local.request被賦值之後,其可以在當前context中作為全局數據使用
- 所謂當前context(the same context)意味著是在同一個greenlet(如果有)中,也就肯定是在同一個線程當中
那麼Werkzeug的Local對象是如何實現這種在相同的context環境下保證數據的全局性和隔離性的呢?
Local的實現
我們先來看下源代碼
# 在有greenlet的情況下,get_indent實際獲取的是greenlet的id,而沒有greenlet的情況下獲取的是thread idtry: from greenlet import getcurrent as get_identexcept ImportError: try: from thread import get_ident except ImportError: from _thread import get_identclass 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()) # 當調用Local對象時,返回對應的LocalProxy def __call__(self, proxy): """Create a proxy for a name.""" return LocalProxy(self, proxy) # Local類中特有的method,用於清空greenlet id或線程id對應的dict數據 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} def __delattr__(self, name): try: del self.__storage__[self.__ident_func__()][name] except KeyError: raise AttributeError(name)
- 這段代碼實際是對
__storage__
dict的封裝,而這個dict中的key使用的就是get_indent函數獲取的id(當有greenlet時使用greenlet id,沒有則使用thread id) __storage__
dict中的value也是一個dict,這個dict就是該greenlet(或者線程)對應的local存儲空間- 通過重新實現
__getattr__
,__setattr__
等魔術方法,我們在greenlet或者線程中使用local對象時,實際會自動獲取greenlet id(或者線程id),從而獲取到對應的dict存儲空間,再通過name key就可以獲取到真正的存儲的對象 - 當我們需要釋放local數據的內存時,可以通過調用release_local()函數來釋放當前context的local數據,如下
>>> loc = Local()>>> loc.foo = 42>>> release_local(loc) # release_local實際調用local對象的__release_local__ method>>> hasattr(loc, foo)False
LocalStack
LocalStack與Local對象類似,都是可以基於Greenlet協程或者線程進行全局存儲的存儲空間(實際LocalStack是對Local進行了二次封裝),區別在於其數據結構是棧的形式。示例如下:
>>> ls = LocalStack()>>> ls.push(42)>>> ls.top42>>> ls.push(23)>>> ls.top23>>> ls.pop()23>>> ls.top42
- 從示例看出Local對象存儲的時候是類似字典的方式,需要有key和value,而LocalStack是基於棧的,通過push和pop來存儲和彈出數據
- 另外,當我們想釋放存儲空間的時候,也可以調用release_local()
LocalStack在Flask框架中會頻繁的出現,其Request Context和App Context的實現都是基於LocalStack,具體可以參考Github上的Flask源碼
LocalProxy
LocalProxy用於代理Local對象和LocalStack對象,而所謂代理就是作為中間的代理人來處理所有針對被代理對象的操作,如下圖所示:
proxy.jpg
接下來我們將重點講下如下內容:
- LocalProxy的使用
- LocalProxy代碼解析
- 為什麼要使用LocalProxy
LocalProxy的使用
初始化LocalProxy有三種方式:
- 通過Local或者LocalStack對象的
__call__
method
from werkzeug.local import Locall = Local()# these are proxiesrequest = l(request)user = l(user)from werkzeug.local import LocalStack_response_local = LocalStack()# this is a proxyresponse = _response_local()
上述代碼直接將對象像函數一樣調用,這是因為Local和LocalStack都實現了__call__
method,這樣其對象就是callable的,因此當我們將對象作為函數調用時,實際調用的是__call__
method,可以看下本文開頭部分的Local的源代碼,會發現__call__
method會返回一個LocalProxy對象
- 通過LocalProxy類進行初始化
l = Local()request = LocalProxy(l, request)
實際上這段代碼跟第一種方式是等價的,但這種方式是最原始的方式,我們在Local的源代碼實現中看到其__call__
method就是通過這種方式生成LocalProxy的
- 使用callable對象作為參數
request = LocalProxy(get_current_request())
通過傳遞一個函數,我們可以自定義如何返回Local或LocalStack對象
那麼LocalProxy是如何實現這種代理的呢?接下來看下源碼解析
LocalProxy代碼解析
下面截取LocalProxy的部分代碼,我們來進行解析
# LocalProxy部分代碼@implements_boolclass 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): """Return the current object. This is useful if you want the real object behind the proxy at a time for performance reasons or because you want to pass the object into a different context. """ # 由於所有Local或LocalStack對象都有__release_local__ method, 所以如果沒有該屬性就表明self.__local為callable對象 if not hasattr(self.__local, __release_local__): return self.__local() try: # 此處self.__local為Local或LocalStack對象 return getattr(self.__local, self.__name__) except AttributeError: raise RuntimeError(no object bound to %s % self.__name__) @property def __dict__(self): try: return self._get_current_object().__dict__ except RuntimeError: raise AttributeError(__dict__) 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] if PY2: __getslice__ = lambda x, i, j: x._get_current_object()[i:j] def __setslice__(self, i, j, seq): self._get_current_object()[i:j] = seq def __delslice__(self, i, j): del self._get_current_object()[i:j] # 截取部分操作符代碼 __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
- 首先在
__init__
method中傳遞的local
參數會被賦予屬性_LocalProxy__local
,該屬性可以通過self.local
進行訪問,關於這一點可以看StackOverflow的問題回答 - LocalProxy通過
_get_current_object
來獲取代理的對象。需要注意的是當初始化參數為callable對象時,則直接調用以返回Local或LocalStack對象,具體看源代碼的注釋。 - 重載了絕大多數操作符,以便在調用LocalProxy的相應操作時,通過
_get_current_object
method來獲取真正代理的對象,然後再進行相應操作
為什麼要使用LocalProxy
可是說了這麼多,為什麼一定要用proxy,而不能直接調用Local或LocalStack對象呢?這主要是在有多個可供調用的對象的時候會出現問題,如下圖:
multiple objects
我們再通過下面的代碼也許可以看出一二:
# use Local object directlyfrom werkzeug.local import LocalStackuser_stack = LocalStack()user_stack.push({name: Bob})user_stack.push({name: John})def get_user(): # do something to get User object and return it return user_stack.pop()# 直接調用函數獲取user對象user = get_user()print user[name]print user[name]
列印結果是:
JohnJohn
再看下使用LocalProxy
# use LocalProxyfrom werkzeug.local import LocalStack, LocalProxyuser_stack = LocalStack()user_stack.push({name: Bob})user_stack.push({name: John})def get_user(): # do something to get User object and return it return user_stack.pop()# 通過LocalProxy使用user對象user = LocalProxy(get_user)print user[name]print user[name]
列印結果是:
JohnBob
怎麼樣,看出區別了吧,直接使用LocalStack對象,user一旦賦值就無法再動態更新了,而使用Proxy,每次調用操作符(這裡[]操作符
用於獲取屬性),都會重新獲取user,從而實現了動態更新user的效果。見下圖:
proxy auto select object
Flask以及Flask的插件很多時候都需要這種動態更新的效果,因此LocalProxy就會非常有用了。
至此,我們針對Local、LocalStack和LocalProxy的概念已經做了詳細闡釋,如果你覺得文章對你有幫助,不妨點個贊吧!
推薦閱讀:
※第四期 · 簡單了解logging模塊 :結合Flask理解和使用try……except與logging模塊
※GitHub 上有什麼使用 Flask 建站的項目嗎?
※有沒有適合沒有python基礎想要用flask寫web開發的書籍介紹?
※Flask實踐:待辦事項(ToDo-List)