1. 程式人生 > >Flask原始碼解讀 --- 請求上下文和request物件

Flask原始碼解讀 --- 請求上下文和request物件

前面第一篇主要記錄了Flask框架,從http請求發起,到返回響應,發生在server和app直接的過程。

裡面有說到,Flask框架有設計了兩種上下文,即應用上下文請求上下文

官方文件裡是說先理解應用上下文比較好,不過我還是覺得反過來,從請求上下文開始記錄比較合適,所以這篇先記錄請求上下文。

什麼是請求上下文

通俗點說,其實上下文就像一個容器,包含了很多你需要的資訊

request和session都屬於請求上下文

request 針對的是http請求作為物件

session針對的是更多是使用者資訊作為物件

上下文的結構

說到上下文這個概念的資料結構,這裡需要先知道,他是運用了一個Stack

的棧結構,也就說,有棧所擁有的特性,push,top,pop等

請求上下文  -----  RequestContext

當一個請求進來的時候,請求上下文環境是如何運作的呢?還是需要來看一下原始碼

上一篇有講到,當一個請求從server傳遞過來的時候,他會呼叫Flask的__call__方法,所以這裡還是回到wsgi_app那部分去講

下面是當wsgi_app被呼叫的時候,最一開始的動作,這裡的ctxcontext的縮寫

class Flask(_PackageBoundObject):

# 省略一部分程式碼

    def wsgi_app(self, environ, start_response):
        ctx = self.request_context(environ)     #上下文變數ctx被賦值為request_context(environ)的值
        ctx.push()                              #

再來看下request_context是一個什麼樣的方法,看看原始碼

看他的返回值,他返回的其實是RequestContext類生成的一個例項物件,看字面意思就知道是一個請求上下文的例項物件了.

這裡可以注意看下他的函式說明,他舉了一個例子,非常簡單,ctx先push,最後再pop,和用with的方法作用是一毛一樣的

這其實就是一個請求到響應最簡單的骨架,側面反映了request生命週期

class Flask(_PackageBoundObject):

#省略部分程式碼

    def request_context(self, environ):
        """ctx = app.request_context(environ)
        ctx.push()
        try:
            do_something_with(request)
        finally:
            ctx.pop()"""
        return RequestContext(self, environ)

繼續往下層看,RequestContext是從ctx.py模組中引入的,所以去找RequestContext的定義

class RequestContext(object):
    """The request context contains all request relevant information.  It is
    created at the beginning of the request and pushed to the
    `_request_ctx_stack` and removed at the end of it.  It will create the
    URL adapter and request object for the WSGI environment provided.

    Do not attempt to use this class directly, instead use
    :meth:`~flask.Flask.test_request_context` and
    :meth:`~flask.Flask.request_context` to create this object."""

#省略部分說明

    def __init__(self, app, environ, request=None):
        self.app = app
        if request is None:
            request = app.request_class(environ)
        self.request = request
        self.url_adapter = app.create_url_adapter(self.request)
        self.flashes = None
        self.session = None

#省略部分程式碼

    def push(self):
        top = _request_ctx_stack.top
        if top is not None and top.preserved:
            top.pop(top._preserved_exc)

        app_ctx = _app_ctx_stack.top
        if app_ctx is None or app_ctx.app != self.app:
            app_ctx = self.app.app_context()
            app_ctx.push()
            self._implicit_app_ctx_stack.append(app_ctx)
        else:
            self._implicit_app_ctx_stack.append(None)

        if hasattr(sys, 'exc_clear'):
            sys.exc_clear()

        _request_ctx_stack.push(self)

注意一下__init__方法,他的第一個引數是app例項物件,所以在前面額app.py檔案內,他的生成方法第一個引數是self,另外,還要傳入environ引數

這樣,回到wsgi_app的函式內部,我們其實已經有了ctx這個變數的值了

所以接下去的一步就是非常重要的ctx.push()

首先會判斷上下文棧的頂端是否有元素,如果是沒元素的,就返回None

如果有元素,會彈出該元素

接著看最後一行,會進行_request_ctx_stack的push動作,引數是self,這裡的self實際上就是上下文例項  ctx,也就是說,把上下文的內容進行壓棧,放到棧頂了。

看到這裡,又引入了一個新的物件 _request_ctx_stack,這其實是一個非常重要的概念,他就是上下文環境的資料結構,也就是結構

繼續找這個物件來自哪裡,發現他來自於同級目錄的globals,開啟後發現,原來所有的上下文環境的定義,都在這裡,怪不得名字取成全域性變數

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


def _lookup_app_object(name):
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return getattr(top, name)


def _find_app():
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError(_app_ctx_err_msg)
    return top.app


# context locals
_request_ctx_stack = LocalStack()                    #請求上下文的資料結構
_app_ctx_stack = LocalStack()                        #引用上下文的資料結構
current_app = LocalProxy(_find_app)                  #從這個開始的4個,都是全域性變數,他們其實通過代理上下文來實現的
request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))
g = LocalProxy(partial(_lookup_app_object, 'g'))

上下文的資料結構分析

看到   _request_ctx_stack是LocalStack的例項物件,那就去找LocalStack的原始碼了,他來自於werkzeug工具包裡面的local模組

class LocalStack(object):

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

#中間省略部分程式碼

    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

#中間省略部分程式碼

    @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
其中最主要的三個方法是,__init__初始化方法, push壓棧方法,以及top元素的訪問方法
__init__初始化方法其實很簡單,他把LocalStack的例項(也就是_request_ctx_stack)的_local屬性,設定為了Local類的例項

所以這裡需要先看一下Local類的定義,他和LocalStack在同一個模組內

class Local(object):
    __slots__ = ('__storage__', '__ident_func__')

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

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

    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}

Local類的例項物件,其實就是包含了2個屬性

一個叫  __storage__  的字典

另一個叫 __ident_func__ 的方法,他這個方法其實是get_ident,這個方法不多說,他是從_thread內建模組裡面匯入的,他的作用是返回執行緒號

這部分有點繞,因為在Local和LocalStack兩個類裡面來回穿梭

Local類的定義看完以後,回過去看LocalStack的push方法

    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
他會先去取 LocalStack例項的_local屬性,也就是Local()例項的stack屬性, 如果沒有這個屬性,則返回None

如果是None的話,則開始建立上下文棧結構,返回值rv代表上下文的整體結構

_local的stack屬性就是一個棧結構
這裡的obj,其實是對應最一開頭的RequestContext裡面的push方法裡的self,也就是,他在push的時候,傳入的物件是上下文RequestContext的例項物件

這裡要再看一下Local類的__setattr__方法了,看看他如何賦值

    def __setattr__(self, name, value):
        ident = self.__ident_func__()
        storage = self.__storage__
        try:
            storage[ident][name] = value
        except KeyError:
            storage[ident] = {name: value}

他其實是一個字典巢狀的形式,因為__storage__本身就是一個字典,而name和value又是一組鍵值

注意,value本身也是一個容器,是list

所以,他的內部形式實際上是 __storage__ ={{ident1:{name1:value1}},{ident2:{name2:value2}},{ident3:{name3:value3}}}

他的取值方式__getattr__  就是__storage__[self.__ident_func__()][name]

這樣每個執行緒對應的上下文棧都是自己本身,不會搞混。


至此,當一個請求上下文環境被建立完之後,到儲存到棧結構頂端的過程,就完成了。

這個時候,棧頂元素裡面已經包含了大量的資訊了,包括像這篇文章裡面最重要的概念的request也包含在裡面了

全域性變數request

來看一下request的定義,他其實是棧頂元素的name屬性,經過LocalProxy形成的一個代理

request = LocalProxy(partial(_lookup_req_object, 'request'))

以上程式碼可以看成是  request = LocalProxy(_request_ctx_stack.top.request) = LocalProxy (_request_ctx_stack._local[stack][-1].request)

也就是棧頂元素內,name叫做request物件的值,而這個值,包含了很多的內容,包括像 HTTP請求頭的資訊,都包括在內,可以提供給全域性使用

但是,這個request物件,早在RequestContext例項建立的時候,就被建立起來了

    def __init__(self, app, environ, request=None):
        self.app = app
        if request is None:
            request = app.request_class(environ)
        self.request = request
        self.url_adapter = app.create_url_adapter(self.request)
        self.flashes = None
        self.session = None
這個是RequestContext類的定義,他的例項有request=app.request_class屬性

例項被壓入上下文棧頂之後,只是通過LocalProxy形成了新的代理後的request,但是內容其實是前面建立的。

所以說,他才能夠使用request這個屬性來進行請求物件的訪問

request來自於Request類

上面的request物件,是通過RequestContext的定義中

request = app.request_class(environ)建立起來的,而request_class = Request類,而Request類則是取自於werkzeuk的 wrappers模組

這個有空再研究了,主要還是和HTTP請求資訊有關係的,比如header parse,ETAG,user Agent之類

class Request(BaseRequest, AcceptMixin, ETagRequestMixin,
              UserAgentMixin, AuthorizationMixin,
              CommonRequestDescriptorsMixin):

    """Full featured request object implementing the following mixins:

    - :class:`AcceptMixin` for accept header parsing
    - :class:`ETagRequestMixin` for etag and cache control handling
    - :class:`UserAgentMixin` for user agent introspection
    - :class:`AuthorizationMixin` for http auth handling
    - :class:`CommonRequestDescriptorsMixin` for common headers
    """

所以說,通過RequestContext上下文環境被壓入棧的過程,flask將app和request進行了掛鉤.

LocalProxy到底是一個什麼東西

LocalProxy的原始碼太長了,就不貼了,關鍵看下LocalProxy和Local及LocalProxy之間的關係

Local和LocalStack的__call__方法,都會將例項,轉化成LocalProxy物件

class LocalStack(object):

#省略部分程式碼

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


class Local(object):

#省略部分程式碼

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

而LocalProxy最關鍵的就是一個_get_current_object方法,一個__getattr__的重寫

@implements_bool
class LocalProxy(object):

#省略部分程式碼

    __slots__ = ('__local', '__dict__', '__name__')

    def __init__(self, local, name=None):
        object.__setattr__(self, '_LocalProxy__local', local)
        object.__setattr__(self, '__name__', name)

    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.
     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)

__getattr__方法和 _get_current_object方法聯合一起,返回了真實物件的name屬性,name就是你想要獲取的資訊.

這樣,你就可以通過request.name 來進行request內部資訊的訪問了。