[TOC]
一直想做源碼閱讀這件事,總感覺難度太高時間太少芯丧,可望不可見芍阎。最近正好時間充裕,決定試試做一下缨恒,并記錄一下學(xué)習(xí)心得谴咸。
首先說明一下,本文研究的Flask版本是0.12骗露。
首先做個小示例岭佳,在pycharm新建flask項目"flask_source"后,默認創(chuàng)建項目入口"flask_source.py"文件萧锉。
運行該文件珊随,在瀏覽器上訪問 http://127.0.0.1:5000/上可以看到“hello,world"內(nèi)容。這是flask_source.py源碼:
#源碼樣例-1
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello World!'
if __name__ == '__main__':
app.run()
本篇博文的目標:閱讀flask源碼了解flask服務(wù)器啟動后柿隙,用戶訪問http://127.0.0.1:5000/后瀏覽“Hello World"這個過程Flask的工作原理及代碼框架叶洞。
WSGI
WSGI,全稱 Web Server Gateway Interface禀崖,或者 Python Web Server Gateway Interface 衩辟,是基于 Python 定義的 Web 服務(wù)器和 Web 應(yīng)用程序或框架之間的一種簡單而通用的接口。WSGI接口的作用是確保HTTP請求能夠轉(zhuǎn)化成python應(yīng)用的一個功能調(diào)用波附,這也就是Gateway的意義所在艺晴,網(wǎng)關(guān)的作用就是在協(xié)議之前進行轉(zhuǎn)換昼钻。
WSGI接口中有一個非常明確的標準,每個Python Web應(yīng)用必須是可調(diào)用callable的對象且返回一個iterator封寞,并實現(xiàn)了app(environ, start_response) 的接口换吧,server 會調(diào)用 application,并傳給它兩個參數(shù):environ 包含了請求的所有信息钥星,start_response 是 application 處理完之后需要調(diào)用的函數(shù)沾瓦,參數(shù)是狀態(tài)碼、響應(yīng)頭部還有錯誤信息谦炒。引用代碼示例:
#源碼樣例-2
# 1. 可調(diào)用對象是一個函數(shù)
def application(environ, start_response):
response_body = 'The request method was %s' % environ['REQUEST_METHOD']
# HTTP response code and message
status = '200 OK'
# 應(yīng)答的頭部是一個列表贯莺,每對鍵值都必須是一個 tuple。
response_headers = [('Content-Type', 'text/plain'),
('Content-Length', str(len(response_body)))]
# 調(diào)用服務(wù)器程序提供的 start_response宁改,填入兩個參數(shù)
start_response(status, response_headers)
# 返回必須是 iterable
return [response_body]
#2. 可調(diào)用對象是一個類實例
class AppClass:
"""這里的可調(diào)用對象就是 AppClass 的實例缕探,使用方法類似于:
app = AppClass()
for result in app(environ, start_response):
do_somthing(result)
"""
def __init__(self):
pass
def __call__(self, environ, start_response):
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
self.start(status, response_headers)
yield "Hello world!\n"
如上圖所示,F(xiàn)lask框架包含了與WSGI Server通信部分和Application本身还蹲。Flask Server本身也包含了一個簡單的WSGI Server(這也是為什么運行flask_source.py可以在瀏覽器訪問的原因)用以開發(fā)測試使用爹耗。在實際的生產(chǎn)部署中,我們將使用apache谜喊、nginx+Gunicorn等方式進行部署潭兽,以適應(yīng)性能要求。
app.run()
下圖是服務(wù)器啟動和處理請求的流程圖斗遏,本節(jié)從分析這個圖開始:
flask的核心組件有兩個Jinjia2和werkzeug山卦。
Jinjia2是一個基于python實現(xiàn)的模板引擎,提供對于HTML的頁面解釋诵次,當然它的功能非常豐富账蓉,可以結(jié)合過濾器、集成逾一、變量铸本、流程邏輯支持等作出非常簡單又很酷炫的的web出來。Flask類實例運行會創(chuàng)造一個Jinjia的環(huán)境遵堵。
在本文使用的樣例中箱玷,我們是直接返回"Hello, world"字符串生成響應(yīng),因此本文將不詳細介紹Jinjia2引擎鄙早,但不否認Jinjia2對于Flask非常重要也非常有用汪茧,值得重點學(xué)習(xí)。不過在源碼學(xué)習(xí)中重點看的是werkzeug限番。
werkzeug
werkzeug是基于python實現(xiàn)的WSGI的工具組件庫舱污,提供對HTTP請求和響應(yīng)的支持,包括HTTP對象封裝弥虐、緩存扩灯、cookie以及文件上傳等等媚赖,并且werkzeug提供了強大的URL路由功能。具體應(yīng)用到Flask中:
- Flask使用werkzeug庫中的Request類和Response類來處理HTTP請求和響應(yīng)
- Flask應(yīng)用使用werkzeug庫中的Map類和Rule類來處理URL的模式匹配珠插,每一個URL模式對應(yīng)一個Rule實例惧磺,這些Rule實例最終會作為參數(shù)傳遞給Map類構(gòu)造包含所有URL模式的一個“地圖”。
- Flask使用SharedDataMiddleware來對靜態(tài)內(nèi)容的訪問支持捻撑,也即是static目錄下的資源可以被外部磨隘,
Flask的示例運行時將與werkzeug進行大量交互:
#源碼樣例-3
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
排除設(shè)置host、port顾患、debug模式這些參數(shù)操作以外番捂,我們重點關(guān)注第一句函數(shù)from werkzeug.serving import run_simple
。
基于wekzeug江解,可以迅速啟動一個WSGI應(yīng)用设预,官方文檔 上有詳細的說明,感興趣的同學(xué)可以自行研究犁河。我們繼續(xù)分析Flask如何與wekzeug調(diào)用鳖枕。
Flask調(diào)用run_simple共傳入5個參數(shù),分別是host=127.0.0.1, port=5001,self=app,use_reloader=False,use_debugger=False
桨螺。按照上述代碼默認啟動的話宾符,在run_simple函數(shù)中,我們執(zhí)行了以下的代碼:
#源碼樣例-4
def inner():
try:
fd = int(os.environ['WERKZEUG_SERVER_FD'])
except (LookupError, ValueError):
fd = None
srv = make_server(hostname, port, application, threaded,
processes, request_handler,
passthrough_errors, ssl_context,
fd=fd)
if fd is None:
log_startup(srv.socket)
srv.serve_forever()
上述的代碼主要的工作是啟動WSGI server并監(jiān)聽指定的端口彭谁。
WSGI server啟動之后吸奴,如果收到新的請求,它的監(jiān)聽在serving.py的run_wsgi
中缠局,執(zhí)行的代碼如下:
#源碼樣例-5
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的內(nèi)容時候強調(diào)的python web實現(xiàn)時需要實現(xiàn)的一個WSGI標準接口,特別是源碼樣例-2
中的第二個參考樣例實現(xiàn)考润,F(xiàn)lask的實現(xiàn)與之類似狭园,當服務(wù)器(gunicorn/uwsgi...)接收到HTTP請求時,它通過werkzeug再execute函數(shù)中通過application_iter = app(environ, start_response)
調(diào)用了Flask應(yīng)用實例app(在run_simple中傳進去的)糊治,實際上調(diào)用的是Flask類的call方法唱矛,因此Flask處理HTTP請求的流程將從call開始,代碼如下:
#源碼樣例-6
def __call__(self, environ, start_response):
"""Shortcut for :attr:`wsgi_app`."""
return self.wsgi_app(environ, start_response)
我們來看一下wsgi_app
這個函數(shù)做了什么工作:
#源碼樣例-7
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的源碼注釋中井辜,開發(fā)者顯著地標明"The actual WSGI application."绎谦,這個函數(shù)的工作流程包括:
-
ctx = self.request_context(environ)
創(chuàng)建請求上下文,并把它推送到棧中粥脚,在“上下文”章節(jié)我們會介紹其數(shù)據(jù)結(jié)構(gòu)窃肠。 -
response = self.full_dispatch_request()
處理請求,通過flask的路由尋找對應(yīng)的視圖函數(shù)進行處理刷允,會在下一章介紹這個函數(shù) - 通過try...except封裝處理步驟2的處理函數(shù)冤留,如果有問題碧囊,拋出500錯誤。
-
ctx.auto_pop(error)
當前請求退棧纤怒。
@app.route('/')
Flask路由的作用是用戶的HTTP請求對應(yīng)的URL能找到相應(yīng)的函數(shù)進行處理糯而。
@app.route('/')
通過裝飾器的方式為對應(yīng)的視圖函數(shù)指定URL,可以一對多泊窘,即一個函數(shù)對應(yīng)多個URL熄驼。
Flask路由的實現(xiàn)時基于werkzeug的URL Routing功能,因此在分析Flask的源碼之前烘豹,首先學(xué)習(xí)一下werkzeug是如何處理路由的谜洽。
werkzeug有兩類數(shù)據(jù)結(jié)構(gòu):Map和Rule:
- Map,主要作用是提供ImmutableDict來存儲URL的Rule實體吴叶。
- Rule阐虚,代表著URL與endpoint一對一匹配的模式規(guī)則。
舉例說明如下蚌卤,假設(shè)在werkzeug中設(shè)置了如下的路由实束,當用戶訪問http://myblog.com/,werkzeug會啟用別名為blog/index
的函數(shù)來處理用戶請求逊彭。
#源碼樣例-8
from werkzeug.routing import Map, Rule, NotFound, RequestRedirect
url_map = Map([
Rule('/', endpoint='blog/index'),
Rule('/<int:year>/', endpoint='blog/archive'),
Rule('/<int:year>/<int:month>/', endpoint='blog/archive'),
Rule('/<int:year>/<int:month>/<int:day>/', endpoint='blog/archive'),
Rule('/<int:year>/<int:month>/<int:day>/<slug>',
endpoint='blog/show_post'),
Rule('/about', endpoint='blog/about_me'),
Rule('/feeds/', endpoint='blog/feeds'),
Rule('/feeds/<feed_name>.rss', endpoint='blog/show_feed')
])
def application(environ, start_response):
urls = url_map.bind_to_environ(environ)
try:
endpoint, args = urls.match()
except HTTPException, e:
return e(environ, start_response)
start_response('200 OK', [('Content-Type', 'text/plain')])
return ['Rule points to %r with arguments %r' % (endpoint, args)]
更多關(guān)于werkzeug路由的細節(jié)可以看官方文檔咸灿。
在上面的示例中,werkzeug完成了url與endpoint的匹配侮叮,endpoint與視圖函數(shù)的匹配將由Flask來完成避矢,F(xiàn)lask通過裝飾器的方式來包裝app.route
诈闺,實際工作函數(shù)是add_url_rule
性宏,其工作流程如下:
- 處理endpoint和構(gòu)建methods芭商,methods默認是GET和OPTIONS诊霹,即默認處理的HTTP請求是GET/OPTIONS方式计呈;
-
self.url_map.add(rule)
更新url_map致份,本質(zhì)是更新werkzeug的url_map -
self.view_functions[endpoint] = view_func
更新view_functions,更新endpoint和視圖函數(shù)的匹配瞎暑,兩者必須一一匹配哀澈,否則報錯AssertionError曙求。
#源碼樣例-9
def add_url_rule(self, rule, endpoint=None, view_func=None, **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
設(shè)置好了Flask的路由之后碍庵,接下來再看看在上一章節(jié)中當用戶請求進來后是如何匹配請求和視圖函數(shù)的。
用戶請求進來后悟狱,F(xiàn)lask類的wsgi_app函數(shù)進行處理静浴,其調(diào)用了full_dispatch_request
函數(shù)進行處理:
#源碼樣例-10
def full_dispatch_request(self):
self.try_trigger_before_first_request_functions()
try:
request_started.send(self)
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)
講一下這個處理的邏輯:
-
self.try_trigger_before_first_request_functions()
觸發(fā)第一次請求之前需要處理的函數(shù),只會執(zhí)行一次挤渐。 -
self.preprocess_request()
觸發(fā)用戶設(shè)置的在請求處理之前需要執(zhí)行的函數(shù)苹享,這個可以通過@app.before_request
來設(shè)置,使用的樣例可以看我之前寫的博文中的示例-11 -
rv = self.dispatch_request()
核心的處理函數(shù)挣菲,包括了路由的匹配富稻,下面會展開來講 -
rv = self.handle_user_exception(e)
處理異常 -
return self.finalize_request(rv)
掷邦,將返回的結(jié)果轉(zhuǎn)換成Response對象并返回。
接下來我們看dispatch_request
函數(shù)椭赋,源碼樣例-11:
#源碼樣例-11
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)
處理的邏輯如下:
-
req = _request_ctx_stack.top.request
獲得請求對象抚岗,并檢查有效性。 - 對于請求的方法進行判斷哪怔,如果HTTP請求時OPTIONS類型且用戶未設(shè)置provide_automatic_options=False宣蔚,則進入默認的OPTIONS請求回應(yīng),否則請求endpoint匹配的函數(shù)執(zhí)行认境,并返回內(nèi)容胚委。
在上述的處理邏輯中,F(xiàn)lask從請求上下文中獲得匹配的rule叉信,這是如何實現(xiàn)的呢亩冬,請看下一節(jié)“上下文”。
Context
對象 | 上下文類型 | 說明 |
---|---|---|
current_app | AppContext | 當前的應(yīng)用對象 |
g | AppContext | 處理請求時用作臨時存儲的對象硼身,當前請求結(jié)束時被銷毀 |
request | RequestContext | 請求對象硅急,封裝了HTTP請求的額內(nèi)容 |
session | RequestContext | 用于存儲請求之間需要共享的數(shù)據(jù) |
純粹的上下文Context理解可以參見知乎的這篇文章,可以認為上下文就是程序的工作環(huán)境佳遂。
Flask的上下文較多营袜,用途也不一致,具體包括:
對象 | 上下文類型 | 說明 |
---|---|---|
current_app | AppContext | 當前的應(yīng)用對象 |
g | AppContext | 處理請求時用作臨時存儲的對象丑罪,當前請求結(jié)束時被銷毀 |
request | RequestContext | 請求對象荚板,封裝了HTTP請求的額內(nèi)容 |
session | RequestContext | 用于存儲請求之間需要共享的數(shù)據(jù) |
引用博文Flask 的 Context 機制:
App Context 代表了“應(yīng)用級別的上下文”,比如配置文件中的數(shù)據(jù)庫連接信息吩屹;Request Context 代表了“請求級別的上下文”跪另,比如當前訪問的 URL。這兩種上下文對象的類定義在 flask.ctx 中祟峦,它們的用法是推入 flask.globals 中創(chuàng)建的 _app_ctx_stack 和 _request_ctx_stack 這兩個單例 Local Stack 中罚斗。因為 Local Stack 的狀態(tài)是線程隔離的,而 Web 應(yīng)用中每個線程(或 Greenlet)同時只處理一個請求宅楞,所以 App Context 對象和 Request Context 對象也是請求間隔離的。
在深入分析上下文源碼之前袱吆,需要特別介紹一下Local厌衙、LocalProxy和LocalStack。這是由werkzeug的locals模塊提供的數(shù)據(jù)結(jié)構(gòu):
Local
#源碼樣例-12
class Local(object):
__slots__ = ('__storage__', '__ident_func__')
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維護了兩個對象:1.stroage绞绒,字典婶希;2.idente_func, 調(diào)用的是thread的get_indent方法,從_thread內(nèi)置模塊導(dǎo)入蓬衡,得到的線程號喻杈。
注意彤枢,這里的stroage的數(shù)據(jù)組織形式是:storage ={ident1:{name1:value1},ident2:{name2:value2},ident3:{name3:value3}}所以取值時候getattr通過self.__storage__[self.__ident_func__()][name]
獲得。
這種設(shè)計確保了Local類實現(xiàn)了類似 threading.local 的效果——多線程或者多協(xié)程情況下全局變量的相互隔離筒饰。
LocalStack
一種基于棧的數(shù)據(jù)結(jié)構(gòu)缴啡,其本質(zhì)是維護了一個Locals對象的代碼示例如下:
#源碼樣例-13
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()
@property
def top(self):
"""The topmost item on the stack. If the stack is empty,
`None` is returned.
"""
try:
return self._local.stack[-1]
except (AttributeError, IndexError):
return None
>>> ls = LocalStack()
>>> ls.push(42)
>>> ls.top
42
>>> ls.push(23)
>>> ls.top
23
>>> ls.pop()
23
>>> ls.top
42
LocalProxy
典型的代理模式實現(xiàn),在構(gòu)造時接受一個callable參數(shù)瓷们,這個參數(shù)被調(diào)用后返回對象是一個Thread Local的對象业栅,對一個LocalProxy對象的所有操作,包括屬性訪問谬晕、方法調(diào)用都會轉(zhuǎn)發(fā)到Callable參數(shù)返回的對象上碘裕。LocalProxy 的一個使用場景是 LocalStack 的 call 方法。比如 my_local_stack 是一個 LocalStack 實例攒钳,那么 my_local_stack() 能返回一個 LocalProxy 對象帮孔,這個對象始終指向 my_local_stack 的棧頂元素。如果棧頂元素不存在不撑,訪問這個 LocalProxy 的時候會拋出 RuntimeError文兢。
LocalProxy的初始函數(shù):
#源碼樣例-14
def __init__(self, local, name=None):
object.__setattr__(self, '_LocalProxy__local', local)
object.__setattr__(self, '__name__', name)
LocalProxy與LocalStack可以完美地結(jié)合起來,首先我們注意LocalStack的call方法:
#源碼樣例-15
def __call__(self):
def _lookup():
rv = self.top
if rv is None:
raise RuntimeError('object unbound')
return rv
return LocalProxy(_lookup)
假設(shè)創(chuàng)建一個LocalStack實例:
#源碼樣例-16
_response_local = LocalStack()
response = _response_local()
然后燎孟,response就成了一個LocalProxy對象禽作,能操作LocalStack的棧頂元素,該對象有兩個元素:_LocalProxy__local(等于_lookup函數(shù))和name(等于None)揩页。
這種設(shè)計簡直碉堡了?醭ァ!1隆萍程!
回到Flask的上下文處理流程,這里引用Flask的核心機制兔仰!關(guān)于請求處理流程和上下文的一張圖進行說明:
Context Create
#源碼樣例-17
def _lookup_req_object(name):
top = _request_ctx_stack.top
if top is None:
raise RuntimeError(_request_ctx_err_msg)
return getattr(top, name)
def _lookup_app_object(name):
top = _app_ctx_stack.top
if top is None:
raise RuntimeError(_app_ctx_err_msg)
return getattr(top, name)
def _find_app():
top = _app_ctx_stack.top
if top is None:
raise RuntimeError(_app_ctx_err_msg)
return top.app
# context locals
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))
g = LocalProxy(partial(_lookup_app_object, 'g'))
從源碼可以了解到以下內(nèi)容:
*. Flask維護的request全局變量_request_ctx_stack 和app全局變量_app_ctx_stack 均為LocalStack結(jié)構(gòu)茫负,這兩個全局變量均是Thread local的棧結(jié)構(gòu)
*. request、session每次都是調(diào)用_request_ctx_stack棧頭部的數(shù)據(jù)來獲取和保存里面的請求上下文信息乎赴。
為什么需要LocalProxy對象忍法,而不是直接引用LocalStack的值?引用flask 源碼解析:上下文的介紹:
這是因為 flask 希望在測試或者開發(fā)的時候榕吼,允許多 app 饿序、多 request 的情況。而 LocalProxy 也是因為這個才引入進來的羹蚣!我們拿 current_app = LocalProxy(_find_app) 來舉例子原探。每次使用 current_app 的時候,他都會調(diào)用 _find_app 函數(shù),然后對得到的變量進行操作咽弦。如果直接使用 current_app = _find_app() 有什么區(qū)別呢徒蟆?區(qū)別就在于,我們導(dǎo)入進來之后型型,current_app 就不會再變化了段审。如果有多 app 的情況,就會出現(xiàn)錯誤输莺。
原文示例代碼:
#源碼樣例-18
from flask import current_app
app = create_app()
admin_app = create_admin_app()
def do_something():
with app.app_context():
work_on(current_app)
with admin_app.app_context():
work_on(current_app)
我的理解是:Flask考慮了一些極端的情況出現(xiàn)戚哎,例如兩個Flask APP通過WSGI的中間件組成一個應(yīng)用,兩個APP同時運行的情況嫂用,因此需要動態(tài)的更新當前的應(yīng)用上下文型凳,而_app_ctx_stack每次都指向棧的頭元素,并且更新頭元素(如果存在刪除再創(chuàng)建)來確保當前運行的上下文(包括請求上下文和應(yīng)用上下文)的準確嘱函。
Stack push
在本文第二章節(jié)介紹Flask運行流程的內(nèi)容時甘畅,我們介紹了wsig_app函數(shù),這個函數(shù)是處理用戶的HTTP請求的往弓,其中有兩句ctx = self.request_context(environ)
和ctx.push()
兩句疏唾。
本質(zhì)上實例了一個RequestContext
,通過WSGI server傳過來的environ來構(gòu)建一個請求的上下文函似。源碼:
#源碼樣例-19
class RequestContext(object):
def __init__(self, app, environ, request=None):
self.app = app
if request is None:
request = app.request_class(environ)
self.request = request
self.url_adapter = app.create_url_adapter(self.request)
self.flashes = None
self.session = None
self.preserved = False
self._preserved_exc = None
self._after_request_functions = []
self.match_request()
def push(self):
top = _request_ctx_stack.top
if top is not None and top.preserved:
top.pop(top._preserved_exc)
app_ctx = _app_ctx_stack.top
if app_ctx is None or app_ctx.app != self.app:
app_ctx = self.app.app_context()
app_ctx.push()
self._implicit_app_ctx_stack.append(app_ctx)
else:
self._implicit_app_ctx_stack.append(None)
if hasattr(sys, 'exc_clear'):
sys.exc_clear()
_request_ctx_stack.push(self)
self.session = self.app.open_session(self.request)
if self.session is None:
self.session = self.app.make_null_session()
Flask的上下文入棧的操作在RequestContext類的push函數(shù):
- 清空_request_ctx_stack棧槐脏;
- 確保當前的Flask實例推入_app_ctx_stack棧;
- 根據(jù)WSGI服務(wù)器傳入的environ構(gòu)建了request(在init函數(shù)完成)撇寞,將該request推入_request_ctx_stack棧顿天;
- 創(chuàng)建session對象。
Stack pop
wsig_app函數(shù)在完成上一小節(jié)上下文入棧之后進行請求分發(fā)蔑担,進行路由匹配尋找視圖函數(shù)處理請求牌废,并生成響應(yīng),此時用戶可以在應(yīng)用程序中import上下文對象作為全局變量進行訪問:
from flask import request,session,request,g
請求完成后啤握,同樣在源碼樣例-7
wsgi_app函數(shù)中可以看到上下文出棧的操作ctx.auto_pop(error)
鸟缕,auto_pop函數(shù)只彈出請求上下文,應(yīng)用上下文仍然存在以應(yīng)對下次的HTTP請求排抬。至此懂从,上下文的管理和操作機制介紹完畢。
Request
接下來繼續(xù)學(xué)習(xí)Flask的請求對象蹲蒲。Flask是基于WSGI服務(wù)器werkzeug傳來的environ參數(shù)來構(gòu)建請求對象的莫绣,檢查發(fā)現(xiàn)environ傳入的是一個字典,在本文的經(jīng)典訪問樣例(返回“Hello, World")中悠鞍,傳入的environ包含的信息包括
"wsgi.multiprocess":"False"
"SERVER_SOFTWARE":"Werkzeug/0.11.15"
"SCRIPT_NAME":""
"REQUEST_METHOD":"GET"
"PATH_INFO":"/favicon.ico"
"SERVER_PROTOCOL":"HTTP/1.1"
"QUERY_STRING":""
"werkzeug.server.shutdown":"<function shutdown_server at 0x0000000003F4FAC8>"
"CONTENT_LENGTH":""
"HTTP_USER_AGENT":"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:55.0) Gecko/20100101 Firefox/55.0"
"HTTP_CONNECTION":"keep-alive"
"SERVER_NAME":"127.0.0.1"
"REMOTE_PORT":"12788"
"wsgi.url_scheme":"http"
"SERVER_PORT":"5000"
"wsgi.input":"<socket._fileobject object at 0x0000000003E18408>"
"HTTP_HOST":"127.0.0.1:5000"
"wsgi.multithread":"False"
"HTTP_ACCEPT":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
"wsgi.version":"(1, 0)"
"wsgi.run_once":"False"
"wsgi.errors":"<open file '<stderr>', mode 'w' at 0x0000000001DD2150>"
"REMOTE_ADDR":"127.0.0.1"
"HTTP_ACCEPT_LANGUAGE":"zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3"
"CONTENT_TYPE":""
"HTTP_ACCEPT_ENCODING":"gzip, deflate"
Flask需要將WSGI server傳進來的上述的字典改造成request對象,它是通過調(diào)用werkzeug.wrappers.Request類來進行構(gòu)建。Request沒有構(gòu)造方法咖祭,且Request繼承了多個類掩宜,在
#源碼樣例-20
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
"""
這里有多重繼承,有多個類負責(zé)處理request的不同內(nèi)容么翰,python的多重繼承按照從下往上牺汤,從左往右的入棧出棧順序進行繼承,且看構(gòu)造方法的參數(shù)匹配浩嫌。在Request的匹配中只有BaseRequest具有構(gòu)造函數(shù)檐迟,其他類只有功能函數(shù),這種設(shè)計模式很特別码耐,但是跟傳統(tǒng)的設(shè)計模式不太一樣追迟,傳統(tǒng)的設(shè)計模式要求是多用組合少用繼承多用拓展少用修改,這種利用多重繼承來達到類功能組合的設(shè)計模式稱為Python的mixin模式骚腥,感覺的同學(xué)可以看看Python mixin模式敦间,接下來重點關(guān)注BaseRequest。
底層的Request功能均由werkzeug來實現(xiàn)束铭,這邊不再一一贅述廓块。
Response
在本文的源碼樣例-1
中,訪問URL地址“http://127.0.0.1” 后契沫,查看返回的response带猴,除了正文文本"Hello, world"外懈万,我們還可以得到一些額外的信息拴清,通過Chrome調(diào)試工具可以看到:
以上的信息都是通過flask服務(wù)器返回,因此钞速,在視圖函數(shù)返回“Hello,World”的響應(yīng)后贷掖,F(xiàn)lask對響應(yīng)做了進一步的包裝。本章節(jié)分析一下Flask如何封裝響應(yīng)信息渴语。
在本文的源碼樣例-10
中用戶的請求由full_dispatch_request
函數(shù)進行處理苹威,其調(diào)用了視圖函數(shù)index()
返回得到rv='Hello, World'
,接下來調(diào)用了finalize_request
函數(shù)進行封裝驾凶,得到其源碼如下:
#源碼樣例-21
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):
status_or_headers = headers = None
if isinstance(rv, tuple):
rv, status_or_headers, headers = rv + (None,) * (3 - len(rv))
if rv is None:
raise ValueError('View function did not return a response')
if isinstance(status_or_headers, (dict, list)):
headers, status_or_headers = status_or_headers, None
if not isinstance(rv, self.response_class):
if isinstance(rv, (text_type, bytes, bytearray)):
rv = self.response_class(rv, headers=headers,
status=status_or_headers)
headers = status_or_headers = None
else:
rv = self.response_class.force_type(rv, request.environ)
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
def process_response(self, response):
ctx = _request_ctx_stack.top
bp = ctx.request.blueprint
funcs = ctx._after_request_functions
if bp is not None and bp in self.after_request_funcs:
funcs = chain(funcs, reversed(self.after_request_funcs[bp]))
if None in self.after_request_funcs:
funcs = chain(funcs, reversed(self.after_request_funcs[None]))
for handler in funcs:
response = handler(response)
if not self.session_interface.is_null_session(ctx.session):
self.save_session(ctx.session, response)
return response
返回信息的封裝順序如下:
-
response = self.make_response(rv)
:根據(jù)視圖函數(shù)返回值生成response對象牙甫。 -
response = self.process_response(response)
:在response發(fā)送給WSGI服務(wù)器錢對于repsonse進行后續(xù)處理,并執(zhí)行當前請求的后續(xù)hooks函數(shù)调违。 -
request_finished.send(self, response=response)
向特定的訂閱者發(fā)送響應(yīng)信息窟哺。關(guān)于Flask的信號機制可以學(xué)習(xí)一下這篇博文,這里不再展開詳細說明技肩。
make_response
該函數(shù)可以根據(jù)不同的輸入得到不同的輸出且轨,即參數(shù)rv的類型是多樣化的,包括:
- str/unicode,如
源碼樣例-1
所示直接返回str后旋奢,將其設(shè)置為body主題后泳挥,調(diào)用response_class
生成其他響應(yīng)信息,例如狀態(tài)碼至朗、headers信息屉符。 - tuple 通過構(gòu)建status_or_headers和headers來進行解析。在
源碼樣例-1
可以修改為返回return make_response(('hello,world!', 202, None))
锹引,得到返回碼也就是202矗钟,即可以在視圖函數(shù)中定義返回狀態(tài)碼和返回頭信息。 - WSGI方法:這個用法沒有找到示例嫌变,不常見吨艇。
- response類實例。視圖函數(shù)可以直接通過調(diào)用make_response接口初澎,該接口可以提供給用戶在視圖函數(shù)中設(shè)計自定制的響應(yīng)秸应,可以參見我之前寫的博文lask自帶的常用組件介紹, 相較于tuple類型碑宴,response能更加豐富和方便地訂制響應(yīng)软啼。
process_response
處理了兩個邏輯:
- 將用戶定義的
after_this_request
方法進行執(zhí)行,同時檢查了是否在blueprint中定義了after_request
和after_app_request
延柠,如果存在祸挪,將其放在執(zhí)行序列; - 保存sesseion贞间。
上述的源碼是flask對于response包裝的第一層外殼贿条,去除這個殼子可以看到,flask實際上調(diào)用了Response類對于傳入的參數(shù)進行包裝增热,其源碼如下:
#源碼樣例-21
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'
嗯整以,基本上 沒啥內(nèi)容,就是繼承了werkzeug.wrappers:Response
峻仇,注意上面的類注釋公黑,作者明確建議使用flask自帶的make_response接口來定義response對象,而不是重新實現(xiàn)它摄咆。werkzeug實現(xiàn)Response的代碼參見教程 這里就不再展開分析了凡蚜。
Config
Flask配置導(dǎo)入對于其他項目的配置導(dǎo)入有很好的借鑒意義,所以我這里還是作為一個單獨的章節(jié)進行源碼學(xué)習(xí)吭从。Flask常用的四種方式進行項目參數(shù)的配置朝蜘,分別是:
#Type1: 直接配置參數(shù)
app.config['SECRET_KEY'] = 'YOUCANNOTGUESSME'
#Type2: 從環(huán)境變量中獲得配置文件名并導(dǎo)入配置參數(shù)
export MyAppConfig=/path/to/settings.cfg #linux
set MyAppConfig=d:\settings.cfg#不能立即生效,不建議windows下通過這種方式獲得環(huán)境變量涩金。
app.config.from_envvar('MyAppConfig')
#Type3: 從對象中獲得配置
class Config(object):
DEBUG = False
TESTING = False
DATABASE_URI = 'sqlite://:memory:'
class ProductionConfig(Config):
DATABASE_URI = 'mysql://user@localhost/foo'
app.config.from_object(ProductionConfig)
print app.config.get('DATABASE_URI')
#Type4: 從文件中獲得配置參數(shù)
# default_config.py
HOST = 'localhost'
PORT = 5000
DEBUG = True
# flask中使用
app.config.from_pyfile('default_config.py')
Flask已經(jīng)默認自帶的配置包括:
['JSON_AS_ASCII', 'USE_X_SENDFILE', 'SESSION_COOKIE_PATH', 'SESSION_COOKIE_DOMAIN', 'SESSION_COOKIE_NAME', 'SESSION_REFRESH_EACH_REQUEST', 'LOGGER_HANDLER_POLICY', 'LOGGER_NAME', 'DEBUG', 'SECRET_KEY', 'EXPLAIN_TEMPLATE_LOADING', 'MAX_CONTENT_LENGTH', 'APPLICATION_ROOT', 'SERVER_NAME', 'PREFERRED_URL_SCHEME', 'JSONIFY_PRETTYPRINT_REGULAR', 'TESTING', 'PERMANENT_SESSION_LIFETIME', 'PROPAGATE_EXCEPTIONS', 'TEMPLATES_AUTO_RELOAD', 'TRAP_BAD_REQUEST_ERRORS', 'JSON_SORT_KEYS', 'JSONIFY_MIMETYPE', 'SESSION_COOKIE_HTTPONLY', 'SEND_FILE_MAX_AGE_DEFAULT', 'PRESERVE_CONTEXT_ON_EXCEPTION', 'SESSION_COOKIE_SECURE', 'TRAP_HTTP_EXCEPTIONS']
其中關(guān)于debug
這個參數(shù)要特別的進行說明谱醇,當我們設(shè)置為app.config["DEBUG"]=True
時候暇仲,flask服務(wù)啟動后進入調(diào)試模式,在調(diào)試模式下服務(wù)器的內(nèi)部錯誤會展示到web前臺枣抱,舉例說明:
app.config["DEBUG"]=True
@app.route('/')
def hello_world():
a=3/0
return 'Hello World!'
打開頁面我們會看到
除了顯示錯誤信息以外熔吗,F(xiàn)lask還支持從web中提供console進行調(diào)試(需要輸入pin碼),破解pin碼很簡單佳晶,這意味著用戶可以對部署服務(wù)器執(zhí)行任意的代碼,所以如果Flask發(fā)布到生產(chǎn)環(huán)境讼载,必須確保
DEBUG=False
轿秧。嗯,有空再寫一篇關(guān)于Flask的安全篇咨堤。另外菇篡,關(guān)于如何配置Flask參數(shù)讓網(wǎng)站更加安全,可以參考這篇博文一喘,寫的很好驱还。
接下來繼續(xù)研究Flask源碼中關(guān)于配置的部分⊥箍耍可以發(fā)現(xiàn)
config
是app
的一個屬性议蟆,而app
是Flask類的一個示例,并且可以通過app.config["DEBUG"]=True
來設(shè)置屬性萎战,可以大膽猜測config應(yīng)該是一個字典類型的類屬性變量咐容,這一點在源碼中驗證了:
#: The configuration dictionary as :class:`Config`. This behaves
#: exactly like a regular dictionary but supports additional methods
#: to load a config from files.
self.config = self.make_config(instance_relative_config)
我們進一步看看make_config
函數(shù)的定義:
def make_config(self, instance_relative=False):
"""Used to create the config attribute by the Flask constructor.
The `instance_relative` parameter is passed in from the constructor
of Flask (there named `instance_relative_config`) and indicates if
the config should be relative to the instance path or the root path
of the application.
.. versionadded:: 0.8
"""
root_path = self.root_path
if instance_relative:
root_path = self.instance_path
return self.config_class(root_path, self.default_config)
config_class = Config
其中有兩個路徑要選擇其中一個作為配置導(dǎo)入的默認路徑,這個用法在上面推薦的博文中用到過蚂维,感興趣的看看戳粒,make_config
真正功能是返回config_class
的函數(shù),而這個函數(shù)直接指向Config
類虫啥,也就是說make_config
返回的是Config
類的實例蔚约。似乎這里面有一些設(shè)計模式在里面,后續(xù)再研究一下涂籽。記下來是Config類的定義:
class Config(dict):
def __init__(self, root_path, defaults=None):
dict.__init__(self, defaults or {})
self.root_path = root_path
root_path代表的是項目配置文件所在的目錄苹祟。defaults是Flask默認的參數(shù),用的是immutabledict數(shù)據(jù)結(jié)構(gòu)又活,是dict的子類苔咪,其中default中定義為:
#: Default configuration parameters.
default_config = ImmutableDict({
'DEBUG': get_debug_flag(default=False),
'TESTING': False,
'PROPAGATE_EXCEPTIONS': None,
'PRESERVE_CONTEXT_ON_EXCEPTION': None,
'SECRET_KEY': None,
'PERMANENT_SESSION_LIFETIME': timedelta(days=31),
'USE_X_SENDFILE': False,
'LOGGER_NAME': None,
'LOGGER_HANDLER_POLICY': 'always',
'SERVER_NAME': None,
'APPLICATION_ROOT': None,
'SESSION_COOKIE_NAME': 'session',
'SESSION_COOKIE_DOMAIN': None,
'SESSION_COOKIE_PATH': None,
'SESSION_COOKIE_HTTPONLY': True,
'SESSION_COOKIE_SECURE': False,
'SESSION_REFRESH_EACH_REQUEST': True,
'MAX_CONTENT_LENGTH': None,
'SEND_FILE_MAX_AGE_DEFAULT': timedelta(hours=12),
'TRAP_BAD_REQUEST_ERRORS': False,
'TRAP_HTTP_EXCEPTIONS': False,
'EXPLAIN_TEMPLATE_LOADING': False,
'PREFERRED_URL_SCHEME': 'http',
'JSON_AS_ASCII': True,
'JSON_SORT_KEYS': True,
'JSONIFY_PRETTYPRINT_REGULAR': True,
'JSONIFY_MIMETYPE': 'application/json',
'TEMPLATES_AUTO_RELOAD': None,
})
我們再看看Config的三個導(dǎo)入函數(shù)from_envvar
,from_pyfile
, from_object
。from_envvar
相當于在from_pyfile
外面包了一層殼子柳骄,從環(huán)境變量中獲得团赏,其函數(shù)注釋中也提到了這一點。而from_pyfile最終也是調(diào)用from_object耐薯。所以我們的重點是看from_object這個函數(shù)的細節(jié)舔清。
from_pyfile源碼中有一句特別難懂丝里,如下。config_file是讀取的文件頭体谒,file_name是文件名稱杯聚。
exec (compile(config_file.read(), filename, 'exec'), d.__dict__)
dict是python的內(nèi)置屬性,包含了該對象(python萬事萬物都是對象)的屬性變量抒痒。類的實例對象的dict只包括類實例后的變量幌绍,而類對象本身的dict還包括包括一些類內(nèi)置屬性和類變量clsvar以及構(gòu)造方法init。
再理解exec函數(shù)故响,exec語句用來執(zhí)行存儲在代碼對象傀广、字符串、文件中的Python語句彩届,eval語句用來計算存儲在代碼對象或字符串中的有效的Python表達式伪冰,而compile語句則提供了字節(jié)編碼的預(yù)編譯。:
exec(object[, globals[, locals]]) #內(nèi)置函數(shù)
其中參數(shù)obejctobj對象可以是字符串(如單一語句樟蠕、語句塊)贮聂,文件對象,也可以是已經(jīng)由compile預(yù)編譯過的代碼對象寨辩,本文就是最后一種吓懈。參數(shù)globals是全局命名空間,用來指定執(zhí)行語句時可以訪問的全局命名空間捣染;參數(shù)locals是局部命名空間骄瓣,用來指定執(zhí)行語句時可以訪問的局部作用域的命名空間。按照這個解釋耍攘,上述的語句其實是轉(zhuǎn)化成了這個語法:
import types
var2=types.ModuleType("test")
exec("A='bb'",var2.__dict__)
把配置文件中定義的參數(shù)寫入到了定義為config Module類型的變量d的內(nèi)置屬性dict中榕栏。
再看看complie函數(shù)compile( str, file, type )
,
compile語句是從type類型(包括’eval’: 配合eval使用蕾各,’single’: 配合單一語句的exec使用扒磁,’exec’: 配合多語句的exec使用)中將str里面的語句創(chuàng)建成代碼對象。file是代碼存放的地方式曲,通常為”妨托。compile語句的目的是提供一次性的字節(jié)碼編譯,就不用在以后的每次調(diào)用中重新進行編譯了吝羞。
from_object源碼中將輸入的參數(shù)進行類型判斷兰伤,如果是object類型的,則說明是通過from_pyfile中傳過來的钧排,只要遍歷from_pyfile傳輸過來的d比變量的內(nèi)置屬性__dict__
即可敦腔。如果輸入的string類型,意味著這個是要從默認的config.py文件中導(dǎo)入恨溜,用戶需要輸入app.config.from_object("config")
進行明確符衔,這時候根據(jù)config直接導(dǎo)入config.py配置找前。
具體的源碼細節(jié)如下:
def from_envvar(self, variable_name, silent=False):
rv = os.environ.get(variable_name)
if not rv:
if silent:
return False
raise RuntimeError('The environment variable %r is not set '
'and as such configuration could not be '
'loaded. Set this variable and make it '
'point to a configuration file' %
variable_name)
return self.from_pyfile(rv, silent=silent)
def from_pyfile(self, filename, silent=False):
filename = os.path.join(self.root_path, filename)
d = types.ModuleType('config')
d.__file__ = filename
try:
with open(filename) as config_file:
exec (compile(config_file.read(), filename, 'exec'), d.__dict__)
except IOError as e:
if silent and e.errno in (errno.ENOENT, errno.EISDIR):
return False
e.strerror = 'Unable to load configuration file (%s)' % e.strerror
raise
self.from_object(d)
return True
def from_object(self, obj):
"""Updates the values from the given object. An object can be of one
of the following two types:
- a string: in this case the object with that name will be imported
- an actual object reference: that object is used directly
Objects are usually either modules or classes. :meth:`from_object`
loads only the uppercase attributes of the module/class. A ``dict``
object will not work with :meth:`from_object` because the keys of a
``dict`` are not attributes of the ``dict`` class.
Example of module-based configuration::
app.config.from_object('yourapplication.default_config')
from yourapplication import default_config
app.config.from_object(default_config)
You should not use this function to load the actual configuration but
rather configuration defaults. The actual config should be loaded
with :meth:`from_pyfile` and ideally from a location not within the
package because the package might be installed system wide.
See :ref:`config-dev-prod` for an example of class-based configuration
using :meth:`from_object`.
:param obj: an import name or object
"""
if isinstance(obj, string_types):
obj = import_string(obj)
for key in dir(obj):
if key.isupper():
self[key] = getattr(obj, key)
根據(jù)源碼分析,from_envvar
和from_pyfile
兩個函數(shù)的輸入配置文件必須是可以執(zhí)行的py文件判族,py文件中變量名必須是大寫躺盛,只有這樣配置變量參數(shù)才能順利的導(dǎo)入到Flask中。