flask 源碼解析:響應

6.flask 源碼解析:響應

response 簡介

在 flask 應用中,我們只需要編寫 view 函數,并不需要直接和響應(response)打交道,flask 會自動生成響應返回給客戶端。

The return value from a view function is automatically converted into a response object for you.
—— Flask docs

我們知道 HTTP 響應分為三個部分:
狀態欄(HTTP 版本、狀態碼和說明)、頭部(以冒號隔開的字符對,用于各種控制和協商)、body(服務端返回的數據)。比如下面訪問一個地址的響應:

HTTP/1.1 200 OK

Access-Control-Allow-Origin: *
Cache-Control: max-age=600
Content-Encoding: gzip
Content-Type: text/html; charset=utf-8
Date: Wed, 15 Feb 2017 07:50:41 GMT
Expires: Wed, 15 Feb 2017 08:00:41 GMT
Last-Modified: Wed, 15 Feb 2017 07:46:56 GMT
Server: GitHub.com
Transfer-Encoding: chunked
X-GitHub-Request-Id: D2A7:7B6B:33C0628:47C44B9:58A40851

<BODY>

flask 自然也會提供所有這些數據的操作,視圖函數就支持返回三個值:第一個是返回的數據,第二個是狀態碼,第三個是頭部字典。比如:

@app.route('/')
def hello_world():
    return 'Hello, World!', 201, {'X-Foo': 'bar'}

這篇文章就講講這背后的魔法。

flask 響應(response)

flask 源碼解析:應用啟動流程 的最后,我們講到 full_dispatch_request 在調用路由的視圖函數之后,會調用 finalize_request 進行最后的處理,在這個方法里就包含了 response 對象的生成和處理邏輯。

finalize_request 的代碼如下:

def finalize_request(self, rv, from_error_handler=False):
    """Given the return value from a view function this finalizes
    the request by converting it into a response and invoking the
    postprocessing functions.  This is invoked for both normal
    request dispatching as well as error handlers.
    """
    response = self.make_response(rv)
    try:
        response = self.process_response(response)
        request_finished.send(self, response=response)
    except Exception:
        if not from_error_handler:
            raise
        self.logger.exception('Request finalizing failed with an '
                              'error while handling an error')
    return response

里面有兩個方法調用:make_response 根據視圖函數的返回值生成 response 對象,process_response 對 response 做一些后續的處理(比如執行 hooks 函數)。我們先來看看 make_response

def make_response(self, rv):
    """Converts the return value from a view function to a real
    response object that is an instance of :attr:`response_class`.
    """
    status_or_headers = headers = None
    if isinstance(rv, tuple):
        rv, status_or_headers, headers = rv + (None,) * (3 - len(rv))

    if isinstance(status_or_headers, (dict, list)):
        headers, status_or_headers = status_or_headers, None

    if not isinstance(rv, self.response_class):
        # When we create a response object directly, we let the constructor
        # set the headers and status.  We do this because there can be
        # some extra logic involved when creating these objects with
        # specific values (like default content type selection).
        if isinstance(rv, (text_type, bytes, bytearray)):
            rv = self.response_class(rv, headers=headers,
                                     status=status_or_headers)
            headers = status_or_headers = None

    if status_or_headers is not None:
        if isinstance(status_or_headers, string_types):
            rv.status = status_or_headers
        else:
            rv.status_code = status_or_headers
    if headers:
        rv.headers.extend(headers)

    return rv

make_response 是視圖函數能返回多個不同數量和類型值的關鍵,因為它能處理這些情況,統一把它們轉換成 response。
如果返回值本身就是 Response 實例,就直接使用它;如果返回值是字符串類型,就把它作為響應的 body,并自動設置狀態碼和頭部信息;
如果返回值是 tuple,會嘗試用 (response, status, headers) 或者 (response, headers) 去解析。

NOTE:因為視圖函數可以返回 Response 對象,因此我們可以直接操作 Response

不管視圖函數返回的是什么,最終都會變成 Response 對象,那么我們就來看看 Response 的定義:

from werkzeug.wrappers import Response as ResponseBase


class Response(ResponseBase):
    """The response object that is used by default in Flask.  Works like the
    response object from Werkzeug but is set to have an HTML mimetype by
    default.  Quite often you don't have to create this object yourself because
    :meth:`~flask.Flask.make_response` will take care of that for you.

    If you want to replace the response object used you can subclass this and
    set :attr:`~flask.Flask.response_class` to your subclass.
    """
    default_mimetype = 'text/html'

Flask 的 Response 類非常簡單,它只是繼承了 werkzeug.wrappers:Response,然后設置默認返回類型為 html。
不過從注釋中,我們得到兩條很有用的信息:

  1. 一般情況下不要直接操作 Response 對象,而是使用 make_response 方法來生成它
  2. 如果需要使用自定義的響應對象,可以覆蓋 flask app 對象的 response_class 屬性。

繼續,下面就要分析 werkzeug 對應的代碼了。

werkzeug response

werkzeug 實現的 response 定義在 werkzeug/wrappers.py 文件中:

class Response(BaseResponse, ETagResponseMixin, ResponseStreamMixin,
               CommonResponseDescriptorsMixin,
               WWWAuthenticateMixin):

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

    - :class:`ETagResponseMixin` for etag and cache control handling
    - :class:`ResponseStreamMixin` to add support for the `stream` property
    - :class:`CommonResponseDescriptorsMixin` for various HTTP descriptors
    - :class:`WWWAuthenticateMixin` for HTTP authentication support
    """

和我們在 flask 請求分析的 Request 類一樣,這里使用了 Mixin 機制。BaseResponse 精簡后的大概框架如下:

class BaseResponse(object):
    """Base response class.  The most important fact about a response object
    is that it's a regular WSGI application.  It's initialized with a couple
    of response parameters (headers, body, status code etc.) and will start a
    valid WSGI response when called with the environ and start response
    callable.
    """

    charset = 'utf-8'
    default_status = 200
    default_mimetype = 'text/plain'
    automatically_set_content_length = True

    def __init__(self, response=None, status=None, headers=None,
                 mimetype=None, content_type=None, direct_passthrough=False):
        pass

BaseResponse 有一些類屬性,定義了默認的值,比如默認字符編碼是 utf-8,默認狀態碼是 200 等。實例化的時候接受的參數有:

  • response: 字符串或者其他 iterable 對象,作為響應的 body
  • status: 狀態碼,可以是整數,也可以是字符串
  • headers: 響應的頭部,可以是個列表,也可以是 werkzeug.datastructures.Headers對象
  • mimetype: mimetype 類型,告訴客戶端響應 body 的格式,默認是文本格式
  • content_type: 響應頭部的 Content-Type 內容

所有這些參數都是可選的,默認情況下會生成一個狀態碼為 200,沒有任何 body 的響應。status、status_code 作為 Response 的屬性,可以直接讀取和修改。body 數據在內部保存為 iterable 的類型,
但是對外也提供了直接讀寫的接口 self.data

    def get_data(self, as_text=False):
        """The string representation of the request body.  Whenever you call
        this property the request iterable is encoded and flattened.
        """
        self._ensure_sequence()
        rv = b''.join(self.iter_encoded())
        if as_text:
            rv = rv.decode(self.charset)
        return rv

    def set_data(self, value):
        """Sets a new string as response.  The value set must either by a
        unicode or bytestring.
        """
        if isinstance(value, text_type):
            value = value.encode(self.charset)
        else:
            value = bytes(value)
        self.response = [value]
        if self.automatically_set_content_length:
            self.headers['Content-Length'] = str(len(value))

    data = property(get_data, set_data, doc='''
        A descriptor that calls :meth:`get_data` and :meth:`set_data`.  This
        should not be used and will eventually get deprecated.
        ''')

body 字符的編碼和長度都是自動設置的,用戶不需要手動處理。

至于頭部的存儲,werkzeug 使用的是類似于字典的 werkzeug.datastructures:Headers 類。在flask 源碼解析:請求這篇文章中,我們沒有詳細
解釋頭部的存儲,那么這篇文章就具體分析一下吧。

Headers 這個類的提供了很多和字典相同的接口:keys、values、iterms,但是和字典的區別在于它保存的值是有序的,而且允許相同 key 的值存在。
為什么這么設計呢?因為著更符合 HTTP 頭部的特性。先來看看有序,在 HTTP 傳送的過程中,如果頭部各個 key-value 鍵值對順序發生變化,有些代理或者客戶端等組件會認為請求被篡改而丟棄或者拒絕請求的處理,所以最好把頭部設置為有序的,用戶按照什么順序設置的,就按照什么順序存儲;再說說相同 key 的問題,這是因為 HTTP 頭部同一個 key 可能有多個 value(比如 Accept、SetCookie頭部)。那么這個看起比較特殊的字典是怎么實現的呢?來看代碼:

class Headers(object):
    """An object that stores some headers.  It has a dict-like interface
    but is ordered and can store the same keys multiple times.
    """

    def __init__(self, defaults=None):
        self._list = []
        if defaults is not None:
            if isinstance(defaults, (list, Headers)):
                self._list.extend(defaults)
            else:
                self.extend(defaults)

    def __getitem__(self, key, _get_mode=False):
        if not _get_mode:
            if isinstance(key, integer_types):
                return self._list[key]
            elif isinstance(key, slice):
                return self.__class__(self._list[key])
        if not isinstance(key, string_types):
            raise exceptions.BadRequestKeyError(key)
        ikey = key.lower()
        for k, v in self._list:
            if k.lower() == ikey:
                return v
        if _get_mode:
            raise KeyError()
        raise exceptions.BadRequestKeyError(key)

可以看到,頭部信息在內部存儲為二元組構成的列表,這樣就能同時保證它的有序性和重復性。一個核心的方法是 __getitem__,它定義了如何獲取頭部中的信息:

  • 通過下標 header[3],直接返回對應未知存儲的鍵值對元組
  • 通過 key,返回 value header['Accept'],返回匹配的第一個 value 值
  • 通過 slice header[3:7],返回另外一個 Headers 對象,保存了 slice 中所有的數據

然后實現 keys()items()pop()setdefault() 等方法讓它表現出來字典的特性,除此之外還有 add()extend()add_header() 等和字典無關的方法方便操作。

自定義 response

如果需要擴展 flask Response 的功能,或者干脆把它替換掉,只要修改 flask app 的 response_class 屬性就可以了,比如:

from flask import Flask, Response

class MyResponse(Response):
    pass

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

推薦閱讀更多精彩內容

  • [TOC]一直想做源碼閱讀這件事,總感覺難度太高時間太少,可望不可見。最近正好時間充裕,決定試試做一下,并記錄一下...
    何柯君閱讀 7,210評論 3 98
  • ? ??在第1章,我們已經了解了Flask的基本知識,如果想要進一步開發更復雜的Flask應用,我們就得了解F...
    懵懂_傻孩紙閱讀 2,987評論 0 4
  • 原文鏈接:Flask Web Development作者的博客譯文鏈接:編程派有翻譯或理解不對的地方,望大家指正!...
    EarlGrey閱讀 12,501評論 1 24
  • flask源碼分析 1. 前言 本文將基于flask 0.1版本(git checkout 8605cc3)來分析...
    甘尼克斯_閱讀 2,742評論 1 0
  • 《梨花賦》 清明將至,又逢踏青之際。清閑散步至梨園,梨樹青紫微紅,光潔溜滑,枝丫中已經點綴出花...
    諸葛智叟閱讀 446評論 2 0