上下文是在Flask開發中的一個核心概念,本文將通過閱讀源碼分享下其原理和實現。
Flask系列文章:
首先,什么是Flask中的上下文?
在Flask中,對一個請求進行處理時,視圖函數一般都會需要請求參數、配置等對象,當然不能對每個請求都傳參一層層到視圖函數(這顯然很不優雅嘛),為此,設計出了上下文機制(比如像我們經常會調用的request就是上下文變量)。
Flask中提供了兩種上下文:
- 請求上下文:包括request和session,保存請求相關的信息
- 程序上下文:包括current_app和g,為了更好的分離程序的狀態,應用起來更加靈活,方便調測等
這四個是上下文變量具體的作用是什么?
- request:封裝客戶端發送的請求報文數據
- session:用於記住請求之間的數據,通過簽名的cookie實現,常用來記住用戶登錄狀態
- current_app:指向處理請求的當前程序實例,比如獲取配置,經常會用current_app.config
- g:當前請求中的全局變量,因為程序上下文的生命周期是伴隨請求上下文產生和銷毀的,所以每次請求都會重設。一般我會在結合鈎子函數在請求處理前使用。
具體是怎么實現的呢?
上下文具體的實現文件:ctx.py
請求上下文對象通過RequestContext類實現,當Flask程序收到請求時,會在wsgi_app()中調用Flask.request_context(),實例化RequestContext()作為請求上下文對象,接着會通過push()方法將請求數據推入到請求上下文堆棧(LocalStack),然后通過full_dispatch_request對象執行視圖函數,調用完成之后通過auto_pop方法來移除。所以,請求上下文的生命周期開始於調用wsgi_app()時,結束與響應生成之后。具體代碼:
def wsgi_app(self, environ, start_response):
ctx = self.request_context(environ)
error = None
try:
try:
ctx.push()
response = self.full_dispatch_request()
except Exception as e:
error = e
response = self.handle_exception(e)
except: # noqa: B001
error = sys.exc_info()[1]
raise
return response(environ, start_response)
finally:
if self.should_ignore_error(error):
error = None
ctx.auto_pop(error)
程序上下文對象通過AppContext類實現,程序上下文的創建方式有兩種:
- 自動創建:在處理請求時,程序上下文會隨着請求上下文一起被創建
- 手動創建:with語句
通過閱讀源碼,可以看到上面兩個上下文對象的push和pop都是通過操作LocalStack對象實現的,那么,LocalStack是怎樣實現的呢?
Werkzeug的LocalStack是棧結構,在 globals.py中定義:
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
具體的實現:
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):
"""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
可以看到:
- LocalStack實現了棧的push、pop和獲取棧頂數據的top數據
- 整個類基於Local類,在構造函數中創建Local類的實例_local,數據是push到Werkzeug提供的Local類中
- 定義
__call__
方法,當實例被調用直接返回棧頂對象的Werkzeug提供的LocalProxy代理,即LocalProxy實例,所以,_request_ctx_stack
和_app_ctx_stack
都是代理。
看到這里,就有以下問題:
Local類是怎樣存儲數據的呢?為啥需要存儲到Local中?
先看下代碼:
try:
from greenlet import getcurrent as get_ident
except ImportError:
try:
from thread import get_ident
except ImportError:
from _thread import get_ident
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):
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)
可以看到,Local構造函數中定義了兩個屬性:
__storage__
:用來保存每個線程的真實數據,對應的存儲結構為->{線程ID:{name:value}}
__ident_func__
:通過get_ident()方法獲取線程ID,可以看到優先會使用Greenlet獲取協程ID,其次是thread模塊的線程ID
Local類在保存數據的同時,記錄對應的線程ID,獲取數據時根據當前線程的id即可獲取到對應數據,這樣就保證了全局使用的上下文對象不會在多個線程中產生混亂,保證了每個線程中上下文對象的獨立和准確。
可以看到,Local類實例被調用時也同樣的被包裝成了一個LocalProxy代理,為什么要用LocalProxy代理?
代理是一種設計模式,通過創建一個代理對象來操作實際對象,簡單理解就是使用一個中間人來轉發操作,Flask上下文處理為什么需要它?
看下代碼實現:
@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__'):
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__)
@property
def __dict__(self):
try:
return self._get_current_object().__dict__
except RuntimeError:
raise AttributeError('__dict__')
def __repr__(self):
try:
obj = self._get_current_object()
except RuntimeError:
return '<%s unbound>' % self.__class__.__name__
return repr(obj)
def __bool__(self):
try:
return bool(self._get_current_object())
except RuntimeError:
return False
def __unicode__(self):
try:
return unicode(self._get_current_object()) # noqa
except RuntimeError:
return repr(self)
def __dir__(self):
try:
return dir(self._get_current_object())
except RuntimeError:
return []
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]
...
通過__getattr__()
、__setitem__()
和__delitem__
會動態的更新實例對象。
再結合上下文對象的調用:
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"))
我們可以很明確的看到:因為上下文的推送和刪除是動態進行的,所以使用代理來動態的獲取上下文對象。
以上,希望你對Flask上下文機制的原理有了清晰的認識。
我的博客即將同步至騰訊雲+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=3emva7t4f08wg