flask 原始碼解析

Wendell_Hu發表於2017-12-15

本文簡單的分析了 Flask 的原始碼,主要關注 WSGI、Flask 物件的資料結構、Flask 應用啟動過程、請求處理過程、檢視函式、URL 的對映、應用上下文和請求上下文。講解這些主題時也不會面面俱到,請按照你閱讀原始碼的需要自行探索。要讀懂本文,你需要較為熟悉 Flask,比如已經用 Flask 寫過一個小專案,並且有一定的閱讀程式碼的能力,並對 web 框架的功能有基本瞭解。

本文會不時更新,最近更新日期:2017年9月10日。

這是 Flask 官方欽定的 Demo 程式碼:

from flask import Flask
app = Flask(__name__)

@app.route(‘/‘)
def index():
    return ‘Hello, world!’

if __name__ == ‘__main__’:
    app.run()
複製程式碼

這篇文章從這個簡單的程式碼開始,簡要介紹了 WSGI、Flask 物件的資料結構、Flask 應用啟動過程、請求處理過程、檢視函式、URL 的對映、request 和 response 類(應用上下文和請求上下文),這些主題涵蓋了一個 web 框架的核心。

WSGI

在使用者發起的請求到達伺服器之後,會被一個 HTTP 伺服器所接收,然後交給 web 應用程式做業務處理,這樣 HTTP 伺服器和 web 應用之間就需要一個介面,在 Python web 開發的世界裡,Python 官方欽定了這個介面並命名為 WSGI,由 PEP333 所規定。只要伺服器和框架都遵守這個約定,那麼就能實現伺服器和框架的任意組合。按照這個規定,一個面向 WSGI 的框架必須要實現這個方法:

def application(environ, start_response)
複製程式碼

在工作過程中,HTTP 伺服器會呼叫上面這個方法,傳入請求資訊,即名為 environ 的字典和 start_response 函式,應用從 environ 中獲取請求資訊,在進行業務處理後呼叫 start_response 設定響應頭,並返回響應體(必須是一個可遍歷的物件,比如列表、字典)給 HTTP 伺服器,HTTP 伺服器再返回響應給使用者。

所以 Flask 作為一個開發 web 應用的 web 框架,負責解決的問題就是:

  1. 作為一個應用,能夠被 HTTP 伺服器所呼叫,必須要有 __call__ 方法
  2. 通過傳入的請求資訊(URL、HTTP 方法等),找到正確的業務處理邏輯,即正確的檢視函式
  3. 處理業務邏輯,這些邏輯可能包括表單檢查、資料庫 CRUD 等(這個在這篇文章裡不會涉及)
  4. 返回正確的響應
  5. 在同時處理多個請求時,還需要保護這些請求,知道應該用哪個響應去匹配哪個請求,即執行緒保護

下面就來看看 Flask 是如何解決這些問題的。

參考閱讀:一起寫一個 web 伺服器,該系列文章能夠讓你基本理解 web 伺服器和框架是如何通過 WSGI 協同工作的。

應用的建立

原始碼閱讀:app.pyFlask 類的程式碼。

Demo 程式碼的第二行建立了一個 Flask 類的例項,傳入的引數是當前模組的名字。我們先來看看 Flask 應用到底是什麼,它的資料結構是怎樣的。

Flask 是這樣一個類:

The flask object implements a WSGI application and acts as the central object. It is passed the name of the module or package of the application. Once it is created it will act as a central registry for the view functions, the URL rules, template configuration and much more.

The name of the package is used to resolve resources from inside the package or the folder the module is contained in depending on if the package parameter resolves to an actual python package (a folder with an __init__.py file inside) or a standard module (just a .py file).

一個 Flask 物件實際上是一個 WSGI 應用。它接收一個模組或包的名字作為引數。它被建立之後,所有的檢視函式、URL 規則、模板設定等都會被註冊到它上面。之所以要傳入模組或包的名字,是為了定位一些資源。

Flask 類有這樣一些屬性:

  • request_class = Request 設定請求的型別
  • response_class = Response 設定響應的型別

這兩個型別都來源於它的依賴庫 werkzeug 並做了簡單的擴充。

Flask 物件的 __init__ 方法如下:

def __init__(self, package_name):
    #: Flask 物件有這樣一個字典來儲存所有的檢視函式
    self.view_functions = {}

    #: 這個字典用來儲存所有的錯誤處理檢視函式
    #: 字典的 key 是錯誤型別碼
    self.error_handlers = {}

    #: 這個列表用來儲存在請求被分派之前應當執行的函式
    self.before_request_funcs = []

    #: 在接收到第一個請求的時候應當執行的函式
    self.before_first_request_funcs = []

    #: 這個列表中的函式在請求完成之後被呼叫,響應物件會被傳給這些函式
    self.after_request_funcs = []

    #: 這裡設定了一個 url_map 屬性,並把它設定為一個 Map 物件
    self.url_map = Map()
複製程式碼

到這裡一個 Flask 物件建立完畢並被變數 app 所指向,其實它就是一個儲存了一些配置資訊,繫結了一些檢視函式並且有個 URL 對映物件(url_map)的物件。但我們還不知道這個 Map 物件是什麼,有什麼作用,從名字上看,似乎其作用是對映 URL 到檢視函式。原始碼第 21 行有 from werkzeug.routing import Map, Rule,那我們就來看看 werkzeug 這個庫中對 Map 的定義:

The map class stores all the URL rules and some configuration parameters. Some of the configuration values are only stored on the Map instance since those affect all rules, others are just defaults and can be overridden for each rule. Note that you have to specify all arguments besides the rules as keyword arguments!

可以看到這個類的物件儲存了所有的 URL 規則和一些配置資訊。由於 werkzeug 的對映機制比較複雜,我們下文中講到對映機制的時候再深入瞭解,現在只要記住 Flask 應用(即一個 Flask 類的例項)儲存了檢視函式,並通過 url_map 這個變數儲存了一個 URL 對映機構就可以了。

應用啟動過程

原始碼閱讀:app.pyFlask 類的程式碼和 werkzeug.serving 的程式碼,特別注意 run_simple BaseWSGIServer WSGIRequestHandler

Demo 程式碼的第 6 行是一個限制,表示如果 Python 直譯器是直接執行該檔案或包的,則執行 Flask 程式:在 Python 中,如果直接執行一個模組或包,那麼直譯器就會把當前模組或包的 __name__ 設定為為 __main_

第 7 行中的 run 方法啟動了 Flask 應用:

def run(self, host=None, port=None, debug=None, **options):
    from werkzeug.serving import run_simple
    if host is None:
        host = '127.0.0.1'
    if port is None:
        server_name = self.config['SERVER_NAME']
        if server_name and ':' in server_name:
            port = int(server_name.rsplit(':', 1)[1])
        else:
            port = 5000
    if debug is not None:
        self.debug = bool(debug)
    options.setdefault('use_reloader', self.debug)
    options.setdefault('use_debugger', self.debug)
    try:
        run_simple(host, port, self, **options)
    finally:
        # reset the first request information if the development server
        # reset normally.  This makes it possible to restart the server
        # without reloader and that stuff from an interactive shell.
        self._got_first_request = False
複製程式碼

可以看到這個方法基本上是在配置引數,實際上啟動伺服器的是 werkzeugrun_simple 方法,該方法在預設情況下啟動了伺服器 BaseWSGIServer,繼承自 Python 標準庫中的 HTTPServer.TCPServer。注意在呼叫 run_simple 時,Flask 物件把自己 self 作為引數傳進去了,這是正確的,因為伺服器在收到請求的時候,必須要知道應該去呼叫誰的 __call__ 方法。

按照標準庫中 HTTPServer.TCPServer 的模式,伺服器必須有一個類來作為 request handler 來處理收到的請求,而不是由 HTTPServer.TCPServer 本身的例項來處理,werkzeug 提供了 WSGIRequestHandler 類來作為 request handler,這個類在被 BaseWSGIServer 呼叫時,會執行這個函式:

def execute(app):
    application_iter = app(environ, start_response)
    try:
        for data in application_iter:
            write(data)
        if not headers_sent:
            write(b'')
    finally:
        if hasattr(application_iter, 'close'):
            application_iter.close()
        application_iter = None
複製程式碼

函式的第一行就是按照 WSGI 要求的,呼叫了 app 並把 environstart_response 傳入。我們再看看 flask 中是如何按照 WSGI 要求對伺服器的呼叫進行呼應的。

def __call__(self, environ, start_response):
    return self.wsgi_app(environ, start_response)

def wsgi_app(self, environ, start_response):
    ctx = self.request_context(environ)
    ctx.push()
    error = None
    try:
        try:
            response = self.full_dispatch_request()
        except Exception as e:
            error = e
            response = self.handle_exception(e)
        return response(environ, start_response)
    finally:
        if self.should_ignore_error(error):
            error = None
        ctx.auto_pop(error)
複製程式碼

可以看到 Flask 按照 WSGI 的要求實現了 __call__ 方法,因此成為了一個可呼叫的物件。但它不是在直接在 __call__ 裡寫邏輯的,而是呼叫了 wsgi_app 方法,這是為了中介軟體的考慮,不展開談了。這個方法返回的 response(environ, start_response) 中,responsewerkzueg.response 類的一個例項,它也是個可以呼叫的物件,這個物件會負責生成最終的可遍歷的響應體,並呼叫 start_response 形成響應頭。

請求處理過程

原始碼閱讀:app.Flask 的程式碼。

def wsgi_app(self, environ, start_response):
    ctx = self.request_context(environ)
    ctx.push()
    error = None
    try:
        try:
            response = self.full_dispatch_request()
        except Exception as e:
            error = e
            response = self.handle_exception(e)
        return response(environ, start_response)
    finally:
        if self.should_ignore_error(error):
            error = None
        ctx.auto_pop(error)
複製程式碼

wsgi_app 方法中裡面的內容就是對請求處理過程的一個高度抽象。

首先,在接收到伺服器傳過來的請求時,Flask 呼叫 request_context 函式建立了一個 RequestContext 請求上下文物件,並把它壓入 _request_ctx_stack 棧。關於上下文和棧的內容下文會再講到,你現在需要知道的是,這些操作是為了 flask 在處理多個請求的時候不會混淆。之後,Flask 會呼叫 full_dispatch_request 方法對這個請求進行分發,開始實際的請求處理過程,這個過程中會生成一個響應物件並最終通過呼叫 response 物件來返回給伺服器。如果當中出錯,就聲稱相應的錯誤資訊。不管是否出錯,最終 Flask 都會把請求上下文推出棧。

full_dispatch_request 是請求分發的入口,我們再來看它的實現:

def full_dispatch_request(self):
    self.try_trigger_before_first_request_functions()
    try:
        rv = self.preprocess_request()
        if rv is None:
            rv = self.dispatch_request()
    except Exception as e:
        rv = self.handle_user_exception(e)
    return self.finalize_request(rv)
複製程式碼

首先呼叫 try_trigger_before_first_request_functions 方法來嘗試呼叫 before_first_request 列表中的函式,如果 Flask_got_first_request 屬性為 Falsebefore_first_request 中的函式就會被執行,執行一次之後,_got_first_request 就會被設定為 True 從而不再執行這些函式。

然後呼叫 preprocess_request 方法,這個方法呼叫 before_request_funcs 列表中所有的方法,如果這些 before_request_funcs 方法中返回了某種東西,那麼就不會真的去分發這個請求。比如說,一個 before_request_funcs 方法是用來檢測使用者是否登入的,如果使用者沒有登入,那麼這個方法就會呼叫 abort 方法從而返回一個錯誤,Flask 就不會分發這個請求而是直接報 401 錯誤。

如果 before_request_funcs 中的函式沒有返回,那麼再呼叫 dispatch_request 方法進行請求分發。這個方法首先會檢視 URL 規則中有沒有相應的 endpointvalue 值,如果有,那麼就呼叫 view_functions 中相應的檢視函式(endpoint 作為鍵值)並把引數值傳入(**req.view_args),如果沒有就由 raise_routing_exception 進行處理。檢視函式的返回值或者錯誤處理檢視函式的返回值會返回給 wsgi_app 方法中的 rv 變數。

def dispatch_request(self):
        req = _request_ctx_stack.top.request
        if req.routing_exception is not None:
            self.raise_routing_exception(req)
        rule = req.url_rule
        if getattr(rule, 'provide_automatic_options', False) \
           and req.method == 'OPTIONS':
            return self.make_default_options_response()
        return self.view_functions[rule.endpoint](**req.view_args)

def finalize_request(self, rv, from_error_handler=False):
    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

def make_response(self, rv):
    if isinstance(rv, self.response_class):
        return rv
    if isinstance(rv, basestring):
        return self.response_class(rv)
    if isinstance(rv, tuple):
        return self.response_class(*rv)
    return self.response_class.force_type(rv, request.environ)
複製程式碼

然後 Flask 就會根據 rv 生成響應,這個 make_response 方法會檢視 rv 是否是要求的返回值型別,否則生成正確的返回型別。比如 Demo 中返回值是字串,就會滿足 isinstance(rv, basestring) 判斷並從字串生成響應。這一步完成之後,Flask 檢視是否有後處理檢視函式需要執行(在 process_response 方法中),並最終返回一個完全處理好的 response 物件。

檢視函式註冊

在請求處理過程一節中,我們已經看到了 Flask 是如何呼叫試圖函式的,這一節我們要關注 Flask 如何構建和請求分派相關的資料結構。我們將主要關注 view_functions,因為其他的資料結構如 before_request_funcs 的構建過程大同小異,甚至更為簡單。我們也將仔細講解在應用的建立一節中遺留的問題,即 url_map 到底是什麼。

Demo 程式碼的第 4 行用修飾器 route 註冊一個檢視函式,這是 Flask 中受到廣泛稱讚的一個設計。在 Flask 類的 route 方法中,可以看到它呼叫了 add_url_rule 方法。

def route(self, rule, **options):
    def decorator(f):
        endpoint = options.pop('endpoint', None)
        self.add_url_rule(rule, endpoint, f, **options)
        return f
    return decorator

def add_url_rule(self, rule, endpoint, **options):
    if endpoint is None:
        endpoint = _endpoint_from_view_func(view_func)
    options['endpoint'] = endpoint
    methods = options.pop('methods', None)
    if methods is None:
        methods = getattr(view_func, 'methods', None) or ('GET',)
    if isinstance(methods, string_types):
        raise TypeError('Allowed methods have to be iterables of strings, '
                        'for example: @app.route(..., methods=["POST"])')
    methods = set(item.upper() for item in methods)

    required_methods = set(getattr(view_func, 'required_methods', ()))

    provide_automatic_options = getattr(view_func,
        'provide_automatic_options', None)

    if provide_automatic_options is None:
        if 'OPTIONS' not in methods:
            provide_automatic_options = True
            required_methods.add('OPTIONS')
        else:
            provide_automatic_options = False

    methods |= required_methods

    rule = self.url_rule_class(rule, methods=methods, **options)
    rule.provide_automatic_options = provide_automatic_options

    self.url_map.add(rule)
    if view_func is not None:
        old_func = self.view_functions.get(endpoint)
        if old_func is not None and old_func != view_func:
            raise AssertionError('View function mapping is overwriting an '
                                 'existing endpoint function: %s' % endpoint)
        self.view_functions[endpoint] = view_func
複製程式碼

這個方法負責註冊檢視函式,並實現 URL 到檢視函式的對映。首先,它要準備好一個檢視函式所支援的 HTTP 方法(基本上一半多的程式碼都是在做這個),然後通過 url_rule_class 建立一個 rule 物件,並把這個物件新增到自己的 url_map 裡。我們那個遺留問題在這裡就得到解答了:rule 物件是一個儲存合法的(Flask 應用所支援的) URL、方法、endpoint(在 **options 中) 及它們的對應關係的資料結構,而 url_map 是儲存這些物件的集合。然後,這個方法將檢視函式新增到 view_functions 當中,endpoint 作為它的鍵,其值預設是函式名。

我們再來深入瞭解一下 rule ,它被定義在 werkzeug.routing.Rule 中:

A Rule represents one URL pattern. There are some options for Rule that change the way it behaves and are passed to the Rule constructor. 一個 Rule 物件代表了一種 URL 模式,可以通過傳入引數來改變它的許多行為。

Rule 的 __init__ 方法為:

def __init__(self, string, defaults=None, subdomain=None, methods=None,
                 build_only=False, endpoint=None, strict_slashes=None,
                 redirect_to=None, alias=False, host=None):
    if not string.startswith('/'):
        raise ValueError('urls must start with a leading slash')
    self.rule = string
    self.is_leaf = not string.endswith('/')

    self.map = None
    self.strict_slashes = strict_slashes
    self.subdomain = subdomain
    self.host = host
    self.defaults = defaults
    self.build_only = build_only
    self.alias = alias
    if methods is None:
        self.methods = None
    else:
        if isinstance(methods, str):
            raise TypeError('param `methods` should be `Iterable[str]`, not `str`')
        self.methods = set([x.upper() for x in methods])
        if 'HEAD' not in self.methods and 'GET' in self.methods:
            self.methods.add('HEAD')
    self.endpoint = endpoint
    self.redirect_to = redirect_to

    if defaults:
        self.arguments = set(map(str, defaults))
    else:
        self.arguments = set()
    self._trace = self._converters = self._regex = self._weights = None
複製程式碼

一個 Rule 被建立後,通過 Mapadd 方法被繫結到 Map 物件上,我們之前說過 flask.url_map 就是一個 Map 物件。

def add(self, rulefactory):
    for rule in rulefactory.get_rules(self):
        rule.bind(self)
        self._rules.append(rule)
        self._rules_by_endpoint.setdefault(rule.endpoint, []).append(rule)
    self._remap = True
複製程式碼

Rulebind 方法的內容,就是新增 Rule 對應的 Map,然後呼叫 compile 方法生成一個正規表示式,compile 方法比較複雜,就不展開了。

def bind(self, map, rebind=False):
    """Bind the url to a map and create a regular expression based on
    the information from the rule itself and the defaults from the map.

    :internal:
    """
    if self.map is not None and not rebind:
        raise RuntimeError('url rule %r already bound to map %r' %
                           (self, self.map))
    self.map = map
    if self.strict_slashes is None:
        self.strict_slashes = map.strict_slashes
    if self.subdomain is None:
        self.subdomain = map.default_subdomain
    self.compile()
複製程式碼

在 Flask 應用收到請求時,這些被繫結到 url_map 上的 Rule 會被檢視,來找到它們對應的檢視函式。這是在請求上下文中實現的,我們先前在 dispatch_request 方法中就見過——我們是從 _request_ctx_stack.top.request 得到 rule 並從這個 rule 找到 endpoint,最終找到用來處理該請求的正確的檢視函式的。所以,接下來我們需要看請求上下的具體實現,並且看一看 Flask 是如何從 url_map 中找到這個 rule 的。

def dispatch_request(self):
    req = _request_ctx_stack.top.request
    if req.routing_exception is not None:
        self.raise_routing_exception(req)
    rule = req.url_rule
    if getattr(rule, 'provide_automatic_options', False) \
       and req.method == 'OPTIONS':
        return self.make_default_options_response()
    return self.view_functions[rule.endpoint](**req.view_args)
複製程式碼

請求上下文

原始碼閱讀:ctx.RequestContext 的程式碼。

請求上下文是如何、在何時被建立的呢?我們先前也見過,在伺服器呼叫應用的時候,Flask 的 wsgi_app 中有這樣的語句,就是建立了請求上下文並壓棧。

def wsgi_app(self, environ, start_response):
    ctx = self.request_context(environ)
    ctx.push()
複製程式碼

request_context 方法非常簡單,就是建立了 RequestContext 類的一個例項,這個類被定義在 flask.ctx 檔案中,它包含了一系列關於請求的資訊,最重要的是它自身的 request 屬性指向了一個 Request 類的例項,這個類繼承自 werkzeug.Request,在 RequestContext 的建立過程中,它會根據傳入的 environ 建立一個 werkzeug.Request 的例項。

接著 RequestContextpush 方法被呼叫,這個方法將自己推到 _request_ctx_stack 的棧頂。

_request_ctx_stack 被定義在 flask.global 檔案中,它是一個 LocalStack 類的例項,是 werkzeug.local 所實現的,如果你對 Python 的 threading 熟悉的話,就會發現這裡實現了執行緒隔離,就是說,在 Python 直譯器執行到 _request_ctx_stack 相關程式碼的時候,直譯器會根據當前程式來選擇正確的例項。

但是,在整個分析 Flask 原始碼的過程中,我們也沒發現 Flask 在被呼叫之後建立過執行緒啊,那麼為什麼要做執行緒隔離呢?看我們開頭提到的 run 函式,其實它可以傳一個 threaded 引數。當不傳這個函式的時候,我們啟動的是 BasicWSGIServer,這個伺服器是單執行緒單程式的,Flask 的執行緒安全自然沒有意義,但是當我們傳入這個引數的時候,我們啟動的是 ThreadedWSGIServer,這時 Flask 的執行緒安全就是有意義的了,在其他多執行緒的伺服器中也是一樣。

總結

一個請求的旅程

這裡,我們通過追蹤一個請求到達伺服器並返回(當然是通過“變成”一個相應)的旅程,串講本文的內容。

  1. 在請求發出之前,Flask 註冊好了所有的檢視函式和 URL 對映,伺服器在自己身上註冊了 Flask 應用。
  2. 請求到達伺服器,伺服器準備好 environmake_response 函式,然後呼叫了自己身上註冊的 Flask 應用。
  3. 應用實現了 WSGI 要求的 application(environ, make_response) 方法。在 Flask 中,這個方法是個被 __call__ 中轉的叫做 wsgi_app 的方法。它首先通過 environ 建立了請求上下文,並將它推入棧,使得 flask 在處理當前請求的過程中都可以訪問到這個請求上下文。
  4. 然後 Flask 開始處理這個請求,依次呼叫 before_first_request_funcs before_request_funcs view_functions 中的函式,並最終通過 finalize_request 生成一個 response 物件,當中只要有函式返回值,後面的函式組就不會再執行,after_request_funcs 進行 response 生成後的後處理。
  5. Flask 呼叫這個 response 物件,最終呼叫了 make_response 函式,並返回了一個可遍歷的響應內容。
  6. 伺服器傳送響應。

Flask 和 werkzeug

在分析過程中,可以很明顯地看出 Flask 和 werkzeug 是強耦合的,實際上 werkzeug 是 Flask 唯一不可或缺的依賴,一些非常細節的工作,其實都是 werkzeug 庫完成的,在本文的例子中,它至少做了這些事情:

  1. 封裝 ResponseRequest 型別供 Flask 使用,在實際開發中,我們在請求和響應物件上的操作,呼叫的其實是 werkzeug 的方法。
  2. 實現 URL 到檢視函式的對映,並且能把 URL 中的引數傳給該檢視函式。我們看到了 Flask 的 url_map 屬性並且看到了它如何繫結檢視函式和錯誤處理函式,但是具體的對映規則的實踐,和在響應過程中的 URL 解析,都是由 werkzeug 完成的。
  3. 通過 LocalProxy 類生成的 _request_ctx_stack 對 Flask 實現執行緒保護。

對於 Flask 的原始碼分析先暫時到這裡。有時間的話,我會分析 Flask 中的模板渲染、import request、藍圖和一些好用的變數及函式,或者深入分析 werkzeug 庫。

參考閱讀

  1. flask 原始碼解析系列文章,你可以在讀完本文了解主線之後,再看這系列文章瞭解更加細節的東西。
  2. 一起寫一個 web 伺服器

文章更新記錄

  • 2017年9月10日:利用 0.12.0 版本進行分析,重新調整了結構和行文順序,增加了許多內容。

相關文章