flask 源碼解析:請求

5.flask 源碼解析:請求

簡介

對于物理鏈路來說,請求只是不同電壓信號,它根本不知道也不需要知道請求格式和內容到底是怎樣的;
對于 TCP 層來說,請求就是傳輸的數據(二進制的數據流),它只要發送給對應的應用程序就行了;
對于 HTTP 層的服務器來說,請求必須是符合 HTTP 協議的內容;
對于 WSGI server 來說,請求又變成了文件流,它要讀取其中的內容,把 HTTP 請求包含的各種信息保存到一個字典中,調用 WSGI app;
對于 flask app 來說,請求就是一個對象,當需要某些信息的時候,只需要讀取該對象的屬性或者方法就行了。

可以看到,雖然是同樣的請求數據,在不同的階段和不同組件看來,是完全不同的形式。因為每個組件都有它本身的目的和功能,這和生活中的事情一個道理:對于同樣的事情,不同的人或者同一個人不同人生階段的理解是不一樣的。

這篇文章呢,我們只考慮最后一個內容,flask 怎么看待請求。

請求

我們知道要訪問 flask 的請求對象非常簡單,只需要 from flask import request

from flask import request

with app.request_context(environ):
    assert request.method == 'POST'

前面一篇文章 已經介紹了這個神奇的變量是怎么工作的,它最后對應了 flask.wrappers:Request 類的對象。
這個類內部的實現雖然我們還不清楚,但是我們知道它接受 WSGI server 傳遞過來的 environ字典變量,并提供了很多常用的屬性和方法可以使用,比如請求的 method、path、args 等。
請求還有一個不那么明顯的特性——它不能被應用修改,應用只能讀取請求的數據。

這個類的定義很簡單,它繼承了 werkzeug.wrappers:Request,然后添加了一些屬性,這些屬性和 flask 的邏輯有關,比如 view_args、blueprint、json 處理等。它的代碼如下:

from werkzeug.wrappers import Request as RequestBase


class Request(RequestBase):
    """
    The request object is a :class:`~werkzeug.wrappers.Request` subclass and
    provides all of the attributes Werkzeug defines plus a few Flask
    specific ones.
    """

    #: The internal URL rule that matched the request.  This can be
    #: useful to inspect which methods are allowed for the URL from
    #: a before/after handler (``request.url_rule.methods``) etc.
    url_rule = None

    #: A dict of view arguments that matched the request.  If an exception
    #: happened when matching, this will be ``None``.
    view_args = None

    @property
    def max_content_length(self):
        """Read-only view of the ``MAX_CONTENT_LENGTH`` config key."""
        ctx = _request_ctx_stack.top
        if ctx is not None:
            return ctx.app.config['MAX_CONTENT_LENGTH']

    @property
    def endpoint(self):
        """The endpoint that matched the request.  This in combination with
        :attr:`view_args` can be used to reconstruct the same or a
        modified URL.  If an exception happened when matching, this will
        be ``None``.
        """
        if self.url_rule is not None:
            return self.url_rule.endpoint

    @property
    def blueprint(self):
        """The name of the current blueprint"""
        if self.url_rule and '.' in self.url_rule.endpoint:
            return self.url_rule.endpoint.rsplit('.', 1)[0]

    @property
    def is_json(self):
        mt = self.mimetype
        if mt == 'application/json':
            return True
        if mt.startswith('application/') and mt.endswith('+json'):
            return True
        return False

這段代碼沒有什難理解的地方,唯一需要說明的就是 @property 裝飾符能夠把類的方法變成屬性,這是 python 中經常見到的用法。

接著我們就要看 werkzeug.wrappers:Request

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
    """

這個方法有一點比較特殊,它沒有任何的 body。但是有多個基類,第一個是 BaseRequest,其他的都是各種 Mixin。
這里要講一下 Mixin 機制,這是 python 多繼承的一種方式,如果你希望某個類可以自行組合它的特性(比如這里的情況),或者希望某個特性用在多個類中,就可以使用 Mixin。
如果我們只需要能處理各種 Accept 頭部的請求,可以這樣做:

class Request(BaseRequest, AcceptMixin)
    pass

但是不要濫用 Mixin,在大多數情況下子類繼承了父類,然后實現需要的邏輯就能滿足需求。

我們先來看看 BaseRequest:

class BaseRequest(object):
    def __init__(self, environ, populate_request=True, shallow=False):
        self.environ = environ
        if populate_request and not shallow:
            self.environ['werkzeug.request'] = self
        self.shallow = shallow

能看到實例化需要的唯一變量是 environ,它只是簡單地把變量保存下來,并沒有做進一步的處理。Request 的內容很多,其中相當一部分是被 @cached_property 裝飾的方法,比如下面這種:

    @cached_property
    def args(self):
        """The parsed URL parameters."""
        return url_decode(wsgi_get_bytes(self.environ.get('QUERY_STRING', '')),
                          self.url_charset, errors=self.encoding_errors,
                          cls=self.parameter_storage_class)

    @cached_property
    def stream(self):
        """The stream to read incoming data from.  Unlike :attr:`input_stream`
        this stream is properly guarded that you can't accidentally read past
        the length of the input.  Werkzeug will internally always refer to
        this stream to read data which makes it possible to wrap this
        object with a stream that does filtering.
        """
        _assert_not_shallow(self)
        return get_input_stream(self.environ)

    @cached_property
    def form(self):
        """The form parameters."""
        self._load_form_data()
        return self.form

    @cached_property
    def cookies(self):
        """Read only access to the retrieved cookie values as dictionary."""
        return parse_cookie(self.environ, self.charset,
                            self.encoding_errors,
                            cls=self.dict_storage_class)

    @cached_property
    def headers(self):
        """The headers from the WSGI environ as immutable
        :class:`~werkzeug.datastructures.EnvironHeaders`.
        """
        return EnvironHeaders(self.environ)

@cached_property 從名字就能看出來,它是 @property 的升級版,添加了緩存功能。我們知道
@property 能把某個方法轉換成屬性,每次訪問屬性的時候,它都會執行底層的方法作為結果返回。
@cached_property 也一樣,區別是只有第一次訪問的時候才會調用底層的方法,后續的方法會直接使用之前返回的值。
那么它是如何實現的呢?我們能在 werkzeug.utils 找到它的定義:

class cached_property(property):

    """A decorator that converts a function into a lazy property.  The
    function wrapped is called the first time to retrieve the result
    and then that calculated result is used the next time you access
    the value.

    The class has to have a `__dict__` in order for this property to
    work.
    """

    # implementation detail: A subclass of python's builtin property
    # decorator, we override __get__ to check for a cached value. If one
    # choses to invoke __get__ by hand the property will still work as
    # expected because the lookup logic is replicated in __get__ for
    # manual invocation.

    def __init__(self, func, name=None, doc=None):
        self.__name__ = name or func.__name__
        self.__module__ = func.__module__
        self.__doc__ = doc or func.__doc__
        self.func = func

    def __set__(self, obj, value):
        obj.__dict__[self.__name__] = value

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        value = obj.__dict__.get(self.__name__, _missing)
        if value is _missing:
            value = self.func(obj)
            obj.__dict__[self.__name__] = value
        return value

這個裝飾器同時也是實現了 __set____get__ 方法的描述器。
訪問它裝飾的屬性,就會調用 __get__ 方法,這個方法先在 obj.__dict__ 中尋找是否已經存在對應的值。如果存在,就直接返回;如果不存在,調用底層的函數
self.func,并把得到的值保存起來,再返回。這也是它能實現緩存的原因:因為它會把函數的值作為屬性保存到對象中。

關于 Request 內部各種屬性的實現,就不分析了,因為它們每個具體的實現都不太一樣,也不復雜,無外乎對 environ 字典中某些字段做一些處理和計算。
接下來回過頭來看看 Mixin,這里只用 AcceptMixin 作為例子:

class AcceptMixin(object):

    @cached_property
    def accept_mimetypes(self):
        return parse_accept_header(self.environ.get('HTTP_ACCEPT'), MIMEAccept)

    @cached_property
    def accept_charsets(self):
        return parse_accept_header(self.environ.get('HTTP_ACCEPT_CHARSET'),
                                   CharsetAccept)

    @cached_property
    def accept_encodings(self):
        return parse_accept_header(self.environ.get('HTTP_ACCEPT_ENCODING'))

    @cached_property
    def accept_languages(self):
        return parse_accept_header(self.environ.get('HTTP_ACCEPT_LANGUAGE'),
                                   LanguageAccept)

AcceptMixin 實現了請求內容協商的部分,比如請求接受的語言、編碼格式、相應內容等。
它也是定義了很多 @cached_property 方法,雖然自己沒有 __init__ 方法,但是也直接使用了
self.environ,因此它并不能直接使用,只能和 BaseRequest 一起出現。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380

推薦閱讀更多精彩內容