Flask源碼剖析

1. 前言

本文將基於flask 0.1版本(git checkout 8605cc3)來分析flask的實現,試圖理清flask中的一些概念,加深讀者對flask的理解,提高對flask的認識。從而,在使用flask過程中,能夠減少困惑,胸有成竹,遇bug而不驚。

在試圖理解flask的設計之前,你知道應該知道以下幾個概念:

  • flask(web框架)是什麼
  • WSGI是什麼
  • jinja2是什麼
  • Werkzeug是什麼

本文將首先回答這些問題,然後再分析flask源碼。

2. 知識準備

2.1 WSGI

下面這張圖來自這裡,通過這張圖,讀者對web框架所處的位置和WSGI協議能夠有一個感性的認識。

WSGI

wikipedia上對WSGI的解釋就比較通俗易懂。為了更好的理解WSGI,我們來看一個例子:

from eventlet import wsginimport eventletnndef hello_world(environ, start_response):n start_response(200 OK, [(Content-Type, text/plain)])n return [Hello, World!rn]nnwsgi.server(eventlet.listen((, 8090)), hello_world)n

我們定義了一個hello_world函數,這個函數接受兩個參數。分別是environ和start_response,我們將這個hello_world傳遞給eventlet.wsgi.server以後, eventlet.wsgi.server在調用hello_world時,會自動傳入environ和start_response這兩個參數,並接受hello_world的返回值。而這,就是WSGI的作用。

也就是說,在python的世界裡,通過WSGI約定了web伺服器怎麼調用web應用程序的代碼,web應用程序需要符合什麼樣的規範,只要web應用程序和web伺服器都遵守WSGI 協議,那麼,web應用程序和web伺服器就可以隨意的組合。這也就是WSGI存在的原因。

WSGI是一種協議,這裡,需要注意兩個相近的概念:

  • uwsgi同WSGI一樣是一種協議
  • 而uWSGI是實現了uwsgi和WSGI兩種協議的web伺服器

2.2 jinja2與Werkzeug

flask依賴jinja2和Werkzeug,為了完全理解flask,我們還需要簡單介紹一下這兩個依賴。

jinja2

Jinja2是一個功能齊全的模板引擎。它有完整的unicode支持,一個可選 的集成沙箱執行環境,被廣泛使用。

jinja2的一個簡單示例如下:

>>> from jinja2 import Templaten>>> template = Template(Hello !)n>>> template.render(name=John Doe)nuHello John Doe!n

Werkzeug

Werkzeug是一個WSGI工具包,它可以作為web框架的底層庫。

我發現Werkzeug的官方文檔介紹特別好,下面這一段摘錄自這裡。

Werkzeug是一個WSGI工具包。WSGI是一個web應用和伺服器通信的協議,web應用可以通過WSGI一起工作。一個基本的」Hello World」WSGI應用看起來是這樣的:

def application(environ, start_response):n start_response(200 OK, [(Content-Type, text/plain)])n return [Hello World!]n

上面這小段代碼就是WSGI協議的約定,它有一個可調用的start_response 。environ包含了所有進來的信息。 start_response用來表明已經收到一個響應。 通過Werkzeug,我們可以不必直接處理請求或者響應這些底層的東西,它已經為我們封裝好了這些。

請求數據需要environ對象,Werkzeug允許我們以一個輕鬆的方式訪問數據。響應對象是一個WSGI應用,提供了更好的方法來創建響應。如下所示:

from werkzeug.wrappers import Responsenn def application(environ, start_response):n response = Response(Hello World!, mimetype=text/plain)n return response(environ, start_response)n

2.3 如何理解wsgi, Werkzeug, flask之間的關係

Flask是一個基於Python開發並且依賴jinja2模板和Werkzeug WSGI服務的一個微型框架,對於Werkzeug,它只是工具包,其用於接收http請求並對請求進行預處理,然後觸發Flask框架,開發人員基於Flask框架提供的功能對請求進行相應的處理,並返回給用戶,如果要返回給用戶複雜的內容時,需要藉助jinja2模板來實現對模板的處理。將模板和數據進行渲染,將渲染後的字元串返回給用戶瀏覽器。

2.4 Flask是什麼,不是什麼

Flask永遠不會包含資料庫層,也不會有表單庫或是這個方面的其它東西。Flask本身只是Werkzeug和Jinja2的之間的橋樑,前者實現一個合適的WSGI應用,後者處理模板。當然,Flask也綁定了一些通用的標準庫包,比如logging。除此之外其它所有一切都交給擴展來實現。

為什麼呢?因為人們有不同的偏好和需求,Flask不可能把所有的需求都囊括在核心裡。大多數web應用會需要一個模板引擎。然而不是每個應用都需要一個SQL資料庫的。

Flask 的理念是為所有應用建立一個良好的基礎,其餘的一切都取決於你自己或者 擴展。

3. Flask源碼分析

Flask的使用非常簡單,官網的例子如下:

from flask import Flasknapp = Flask(__name__)nn@app.route("/")ndef hello():n return "Hello World!"nnif __name__ == "__main__":n app.run()n

每當我們需要創建一個flask應用時,我們都會創建一個Flask對象:

app = Flask(__name__)n

下面看一下Flask對象的__init__方法,如果不考慮jinja2相關,核心成員就下面幾個:

class Flask:n def __init__(self, package_name):nn self.package_name = package_namen self.root_path = _get_package_path(self.package_name)nn self.view_functions = {}n self.error_handlers = {}n self.before_request_funcs = []n self.after_request_funcs = []n self.url_map = Map()n

我們把目光聚集到後面幾個成員,view_functions中保存了視圖函數(處理用戶請求的函數,如上面的hello()),error_handlers中保存了錯誤處理函數,before_request_funcs和after_request_funcs保存了請求的預處理函數和後處理函數。

self.url_map用以保存URI到視圖函數的映射,即保存app.route()這個裝飾器的信息,如下所示:

def route(...):n def decorator(f):n self.add_url_rule(rule, f.__name__, **options)n self.view_functions[f.__name__] = fn return fn return decoratorn

上面說到的是初始化部分,下面看一下執行部分,當我們執行app.run()時,調用堆棧如下:

app.run()n run_simple(host, port, self, **options)n __call__(self, environ, start_response)n wsgi_app(self, environ, start_response)n

wsgi_app是flask核心:

def wsgi_app(self, environ, start_response):n with self.request_context(environ):n rv = self.preprocess_request()n if rv is None:n rv = self.dispatch_request()n response = self.make_response(rv)n response = self.process_response(response)n return response(environ, start_response)n

可以看到,wsgi_app這個函數的作用就是先調用所有的預處理函數,然後分發請求,再調用所有後處理函數,最後返回response。

看一下dispatch_request函數的實現,因為,這裡有flask的錯誤處理邏輯:

def dispatch_request(self):n try:n endpoint, values = self.match_request()n return self.view_functions[endpoint](**values)n except HTTPException, e:n handler = self.error_handlers.get(e.code)n if handler is None:n return en return handler(e)n except Exception, e:n handler = self.error_handlers.get(500)n if self.debug or handler is None:n raisen return handler(e)n

如果出現錯誤,則根據相應的error code,調用不同的錯誤處理函數。

上面這段簡單的源碼分析,就已經將Flask幾個核心變數和核心函數串聯起來了。其實,我們這裡扣出來的幾段代碼,也就是Flask的核心代碼。畢竟,Flask的0.1版本包含大量注釋以後,也才六百行代碼。

4. flask的魔法

如果讀者打開flask.py文件,將看到我前面的源碼分析幾乎已經覆蓋了所有重要的代碼。但是,細心的讀者會看到,在Flask.py文件的末尾處,有以下幾行代碼:

# context localsn_request_ctx_stack = LocalStack()ncurrent_app = LocalProxy(lambda: _request_ctx_stack.top.app)nrequest = LocalProxy(lambda: _request_ctx_stack.top.request)nsession = LocalProxy(lambda: _request_ctx_stack.top.session)ng = LocalProxy(lambda: _request_ctx_stack.top.g)n

這是我們得以方便的使用flask開發的魔法,也是flask源碼中的難點。在分析之前,我們先看一下它們的作用。

在flask的開發過程中,我們可以通過如下方式訪問url中的參數:

from flask import requestnn@app.route(/)ndef hello():n name = request.args.get(name, None)n

看起來request像是一個全局變數,那麼,一個全局變數為什麼可以在一個多線程環境中隨意使用呢,下面就隨我來一探究竟吧!

先看一下全局變數_request_ctx_stack的定義:

_request_ctx_stack = LocalStack()n

正如它LocalStack()的名字所暗示的那樣,_request_ctx_stack是一個棧。顯然,一個棧肯定會有push 、pop和top函數,如下所示:

class LocalStack(object):nn def __init__(self):n self._local = Local()n n def push(self, obj):n rv = getattr(self._local, stack, None)n if rv is None:n self._local.stack = rv = []n rv.append(obj)n return rvnn def pop(self):n stack = getattr(self._local, stack, None)n if stack is None:n return Nonen elif len(stack) == 1:n release_local(self._local)n return stack[-1]n else:n return stack.pop()n

按照我們的理解,要實現一個棧,那麼LocalStack類應該有一個成員變數,是一個list,然後通過 這個list來保存棧的元素。然而,LocalStack並沒有一個類型是list的成員變數, LocalStack僅有一個成員變數self._local = Local()。

順藤摸瓜,我們來到了Werkzeug的源碼中,到達了Local類的定義處:

class Local(object):nn def __init__(self):n object.__setattr__(self, __storage__, {})n object.__setattr__(self, __ident_func__, get_ident)nn def __getattr__(self, name):n try:n return self.__storage__[self.__ident_func__()][name]n except KeyError:n raise AttributeError(name)nn def __setattr__(self, name, value):n ident = self.__ident_func__()n storage = self.__storage__n try:n storage[ident][name] = valuen except KeyError:n storage[ident] = {name: value}n

需要注意的是,Local類有兩個成員變數,分別是__storage__和__ident_func__,其中,前者 是一個字典,後者是一個函數。這個函數的含義是,獲取當前線程的id(或協程的id)。

此外,我們注意到,Local類自定義了__getattr__和__setattr__這兩個方法,也就是說,我們在操作self.local.stack時, 會調用__setattr__和__getattr__方法。

_request_ctx_stack = LocalStack()n _request_ctx_stack.push(item)n # 注意,這裡賦值的時候,會調用__setattr__方法n self._local.stack = rv = [] ==> __setattr__(self, name, value)n

而__setattr的定義如下:

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

在__setattr__中,通過__ident_func__獲取到了一個key,然後進行賦值。自此,我們可以知道, LocalStack是一個全局字典,或者說是一個名字空間。這個名字空間是所有線程共享的。 當我們訪問字典中的某個元素的時候,會通過__getattr__進行訪問,__getattr__先通過線程id, 找當前這個線程的數據,然後進行訪問。

欄位的內容如下:

{thread_id:{stack:[]}}nn{thread_id1:{stack:[_RequestContext()]},n thread_id2:{stack:[_RequestContext()]}}n

最後,我們來看一下其他幾個全局變數:

current_app = LocalProxy(lambda: _request_ctx_stack.top.app)nrequest = LocalProxy(lambda: _request_ctx_stack.top.request)nsession = LocalProxy(lambda: _request_ctx_stack.top.session)ng = LocalProxy(lambda: _request_ctx_stack.top.g)n

讀者可以自行看一下LocalProxy的源碼,LocalProxy僅僅是一個代理(可以想像設計模式中的代理模式)。

通過LocalStack和LocalProxy這樣的Python魔法,每個線程訪問當前請求中的數據(request, session)時, 都好像都在訪問一個全局變數,但是,互相之間又互不影響。這就是Flask為我們提供的便利,也是我們 選擇Flask的理由!

5. 總結

在這篇文章中,我們簡單地介紹了WSGI, jinja2和Werkzeug,詳細介紹了Flask在web開發中所處的位置和發揮的作用。最後,深入Flask的源碼,了解了Flask的實現。

6. 參考資料

  1. Web Server Gateway Interface
  2. 歡迎來到 Jinja2 - Jinja2 2.7 documentation
  3. Werkzeug 文檔概覽 - Werkzeug 0.9.4 文檔
  4. zlovezl.cn/articles/cha
  5. hackerxu.com/2015/05/10

推薦閱讀:

如何理解 Tornado ?
Django 有哪些局限性?
為什麼 Python 裡面的 range 不包含上界?
如何理解 CGI, WSGI?
python搭建網站和cms搭建網站哪個更快,各有何優勢?

TAG:Python | Python框架 | Flask |