Flask原始碼剖析
- 前言
本文將基於flask 0.1版本(git checkout 8605cc3)來分析flask的實現,試圖理清flask中的一些概念,加深讀者對flask的理解,提高對flask的認識。從而,在使用flask過程中,能夠減少困惑,胸有成竹,遇bug而不驚。
在試圖理解flask的設計之前,你知道應該知道以下幾個概念:
flask(web框架)是什麼
WSGI是什麼
jinja2是什麼
Werkzeug是什麼
本文將首先回答這些問題,然後再分析flask原始碼。
- 知識準備
2.1 WSGI
下面這張圖來自這裡,通過這張圖,讀者對web框架所處的位置和WSGI協議能夠有一個感性的認識。
WSGI
wikipedia上對WSGI的解釋就比較通俗易懂。為了更好的理解WSGI,我們來看一個例子:
from eventlet import wsgi
import eventlet
def hello_world(environ, start_response):
start_response(‘200 OK’, [(‘Content-Type’, ‘text/plain’)])
return [‘Hello, World!\r\n’]
wsgi.server(eventlet.listen((”, 8090)), hello_world)
我們定義了一個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 Template
template = Template(‘Hello !’)
template.render(name=’John Doe’)
u’Hello John Doe!’
Werkzeug
Werkzeug是一個WSGI工具包,它可以作為web框架的底層庫。
我發現Werkzeug的官方文件介紹特別好,下面這一段摘錄自這裡。
Werkzeug是一個WSGI工具包。WSGI是一個web應用和伺服器通訊的協議,web應用可以通過WSGI一起工作。一個基本的”Hello World”WSGI應用看起來是這樣的:
def application(environ, start_response):
start_response(‘200 OK’, [(‘Content-Type’, ‘text/plain’)])
return [‘Hello World!’]
上面這小段程式碼就是WSGI協議的約定,它有一個可呼叫的start_response 。environ包含了所有進來的資訊。 start_response用來表明已經收到一個響應。 通過Werkzeug,我們可以不必直接處理請求或者響應這些底層的東西,它已經為我們封裝好了這些。
請求資料需要environ物件,Werkzeug允許我們以一個輕鬆的方式訪問資料。響應物件是一個WSGI應用,提供了更好的方法來建立響應。如下所示:
from werkzeug.wrappers import Response
def application(environ, start_response):
response = Response(‘Hello World!’, mimetype=’text/plain’)
return response(environ, start_response)
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 的理念是為所有應用建立一個良好的基礎,其餘的一切都取決於你自己或者 擴充套件。
- Flask原始碼分析
Flask的使用非常簡單,官網的例子如下:
from flask import Flask
app = Flask(name)
@app.route(“/”)
def hello():
return “Hello World!”
if name == “main“:
app.run()
每當我們需要建立一個flask應用時,我們都會建立一個Flask物件:
app = Flask(name)
下面看一下Flask物件的init方法,如果不考慮jinja2相關,核心成員就下面幾個:
class Flask:
def init(self, package_name):
self.package_name = package_name
self.root_path = _get_package_path(self.package_name)
self.view_functions = {}
self.error_handlers = {}
self.before_request_funcs = []
self.after_request_funcs = []
self.url_map = Map()
我們把目光聚集到後面幾個成員,view_functions中儲存了檢視函式(處理使用者請求的函式,如上面的hello()),error_handlers中儲存了錯誤處理函式,before_request_funcs和after_request_funcs儲存了請求的預處理函式和後處理函式。
self.url_map用以儲存URI到檢視函式的對映,即儲存app.route()這個裝飾器的資訊,如下所示:
def route(…):
def decorator(f):
self.add_url_rule(rule, f.name, **options)
self.view_functions[f.name] = f
return f
return decorator
上面說到的是初始化部分,下面看一下執行部分,當我們執行app.run()時,呼叫堆疊如下:
app.run()
run_simple(host, port, self, **options)
call(self, environ, start_response)
wsgi_app(self, environ, start_response)
wsgi_app是flask核心:
def wsgi_app(self, environ, start_response):
with self.request_context(environ):
rv = self.preprocess_request()
if rv is None:
rv = self.dispatch_request()
response = self.make_response(rv)
response = self.process_response(response)
return response(environ, start_response)
可以看到,wsgi_app這個函式的作用就是先呼叫所有的預處理函式,然後分發請求,再呼叫所有後處理函式,最後返回response。
看一下dispatch_request函式的實現,因為,這裡有flask的錯誤處理邏輯:
def dispatch_request(self):
try:
endpoint, values = self.match_request()
return self.view_functionsendpoint
except HTTPException, e:
handler = self.error_handlers.get(e.code)
if handler is None:
return e
return handler(e)
except Exception, e:
handler = self.error_handlers.get(500)
if self.debug or handler is None:
raise
return handler(e)
如果出現錯誤,則根據相應的error code,呼叫不同的錯誤處理函式。
上面這段簡單的原始碼分析,就已經將Flask幾個核心變數和核心函式串聯起來了。其實,我們這裡扣出來的幾段程式碼,也就是Flask的核心程式碼。畢竟,Flask的0.1版本包含大量註釋以後,也才六百行程式碼。
- flask的魔法
如果讀者開啟flask.py檔案,將看到我前面的原始碼分析幾乎已經覆蓋了所有重要的程式碼。但是,細心的讀者會看到,在Flask.py檔案的末尾處,有以下幾行程式碼:
context locals
_request_ctx_stack = LocalStack()
current_app = LocalProxy(lambda: _request_ctx_stack.top.app)
request = LocalProxy(lambda: _request_ctx_stack.top.request)
session = LocalProxy(lambda: _request_ctx_stack.top.session)
g = LocalProxy(lambda: _request_ctx_stack.top.g)
這是我們得以方便的使用flask開發的魔法,也是flask原始碼中的難點。在分析之前,我們先看一下它們的作用。
在flask的開發過程中,我們可以通過如下方式訪問url中的引數:
from flask import request
@app.route(‘/’)
def hello():
name = request.args.get(‘name’, None)
看起來request像是一個全域性變數,那麼,一個全域性變數為什麼可以在一個多執行緒環境中隨意使用呢,下面就隨我來一探究竟吧!
先看一下全域性變數_request_ctx_stack的定義:
_request_ctx_stack = LocalStack()
正如它LocalStack()的名字所暗示的那樣,_request_ctx_stack是一個棧。顯然,一個棧肯定會有push 、pop和top函式,如下所示:
class LocalStack(object):
def __init__(self):
self._local = Local()
def push(self, obj):
rv = getattr(self._local, 'stack', None)
if rv is None:
self._local.stack = rv = []
rv.append(obj)
return rv
def pop(self):
stack = getattr(self._local, 'stack', None)
if stack is None:
return None
elif len(stack) == 1:
release_local(self._local)
return stack[-1]
else:
return stack.pop()
按照我們的理解,要實現一個棧,那麼LocalStack類應該有一個成員變數,是一個list,然後通過 這個list來儲存棧的元素。然而,LocalStack並沒有一個型別是list的成員變數, LocalStack僅有一個成員變數self._local = Local()。
順藤摸瓜,我們來到了Werkzeug的原始碼中,到達了Local類的定義處:
class Local(object):
def __init__(self):
object.__setattr__(self, '__storage__', {})
object.__setattr__(self, '__ident_func__', get_ident)
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類有兩個成員變數,分別是storage和ident_func,其中,前者 是一個字典,後者是一個函式。這個函式的含義是,獲取當前執行緒的id(或協程的id)。
此外,我們注意到,Local類自定義了getattr和setattr這兩個方法,也就是說,我們在操作self.local.stack時, 會呼叫setattr和getattr方法。
_request_ctx_stack = LocalStack()
_request_ctx_stack.push(item)
# 注意,這裡賦值的時候,會呼叫setattr方法
self._local.stack = rv = [] ==> setattr(self, name, value)
而__setattr的定義如下:
def setattr(self, name, value):
ident = self.ident_func()
storage = self.storage
try:
storage[ident][name] = value
except KeyError:
storage[ident] = {name: value}
在setattr中,通過ident_func獲取到了一個key,然後進行賦值。自此,我們可以知道, LocalStack是一個全域性字典,或者說是一個名字空間。這個名字空間是所有執行緒共享的。 當我們訪問字典中的某個元素的時候,會通過getattr進行訪問,getattr先通過執行緒id, 找當前這個執行緒的資料,然後進行訪問。
欄位的內容如下:
{‘thread_id’:{‘stack’:[]}}
{‘thread_id1’:{‘stack’:[_RequestContext()]},
‘thread_id2’:{‘stack’:[_RequestContext()]}}
最後,我們來看一下其他幾個全域性變數:
current_app = LocalProxy(lambda: _request_ctx_stack.top.app)
request = LocalProxy(lambda: _request_ctx_stack.top.request)
session = LocalProxy(lambda: _request_ctx_stack.top.session)
g = LocalProxy(lambda: _request_ctx_stack.top.g)
讀者可以自行看一下LocalProxy的原始碼,LocalProxy僅僅是一個代理(可以想象設計模式中的代理模式)。
通過LocalStack和LocalProxy這樣的Python魔法,每個執行緒訪問當前請求中的資料(request, session)時, 都好像都在訪問一個全域性變數,但是,互相之間又互不影響。這就是Flask為我們提供的便利,也是我們 選擇Flask的理由!
- 總結
在這篇文章中,我們簡單地介紹了WSGI, jinja2和Werkzeug,詳細介紹了Flask在web開發中所處的位置和發揮的作用。最後,深入Flask的原始碼,瞭解了Flask的實現。
- 參考資料