flask源碼分析

flask源碼分析

1. 前言

本文將基于flask 0.1版本(git checkout 8605cc3)來(lái)分析flask的實(shí)現(xiàn),試圖理清flask中的一些概念漓库,加深讀者對(duì)flask的理解,提高對(duì)flask的認(rèn)識(shí)。從而,在使用flask過(guò)程中睡蟋,能夠減少困惑,胸有成竹枷颊,遇bug而不驚戳杀。

在試圖理解flask的設(shè)計(jì)之前,你知道應(yīng)該知道以下幾個(gè)概念:

flask(web框架)是什么

WSGI是什么

jinjia2是什么

Werkzeug是什么

本文將首先回答這些問(wèn)題夭苗,然后再分析flask源碼信卡。

2. 知識(shí)準(zhǔn)備

2.1 WSGI

下面這張圖來(lái)自這里,通過(guò)這張圖听诸,讀者對(duì)web框架所處的位置和WSGI協(xié)議能夠有一個(gè)感性的認(rèn)識(shí)坐求。

WSGI

wikipedia上對(duì)WSGI的解釋就比較通俗易懂。為了更好的理解WSGI晌梨,我們來(lái)看一個(gè)例子:

fromeventletimportwsgiimporteventletdefhello_world(environ,start_response):start_response('200 OK',[('Content-Type','text/plain')])return['Hello, World!\r\n']wsgi.server(eventlet.listen(('',8090)),hello_world)

我們定義了一個(gè)hello_world函數(shù)桥嗤,這個(gè)函數(shù)接受兩個(gè)參數(shù)须妻。分別是environ和start_response,我們將這個(gè)hello_world傳遞給eventlet.wsgi.server以后泛领,eventlet.wsgi.server在調(diào)用hello_world時(shí)荒吏,會(huì)自動(dòng)傳入environ和start_response這兩個(gè)參數(shù),并接受hello_world的返回值渊鞋。而這绰更,就是WSGI的作用。

也就是說(shuō)锡宋,在python的世界里儡湾,通過(guò)WSGI約定了web服務(wù)器怎么調(diào)用web應(yīng)用程序的代碼,web應(yīng)用程序需要符合什么樣的規(guī)范执俩,只要web應(yīng)用程序和web服務(wù)器都遵守WSGI

協(xié)議徐钠,那么,web應(yīng)用程序和web服務(wù)器就可以隨意的組合役首。這也就是WSGI存在的原因尝丐。

WSGI是一種協(xié)議,這里衡奥,需要注意兩個(gè)相近的概念:

uwsgi同WSGI一樣是一種協(xié)議

而uWSGI是實(shí)現(xiàn)了uwsgi和WSGI兩種協(xié)議的web服務(wù)器

2.2 jinjia2與Werkzeug

flask依賴jinjia2和Werkzeug爹袁,為了完全理解flask,我們還需要簡(jiǎn)單介紹一下這兩個(gè)依賴矮固。

jinjia2

Jinja2是一個(gè)功能齊全的模板引擎失息。它有完整的unicode支持,一個(gè)可選的集成沙箱執(zhí)行環(huán)境档址,被廣泛使用根时。

jinjia2的一個(gè)簡(jiǎn)單示例如下:

>>>fromjinja2importTemplate>>>template=Template('Hello !')>>>template.render(name='John Doe')u'Hello John Doe!'

Werkzeug

Werkzeug是一個(gè)WSGI工具包,它可以作為web框架的底層庫(kù)辰晕。

我發(fā)現(xiàn)Werkzeug的官方文檔介紹特別好,下面這一段摘錄自這里确虱。

Werkzeug是一個(gè)WSGI工具包含友。WSGI是一個(gè)web應(yīng)用和服務(wù)器通信的協(xié)議,web應(yīng)用可以通過(guò)WSGI一起工作校辩。一個(gè)基本的”Hello World”WSGI應(yīng)用看起來(lái)是這樣的:

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

上面這小段代碼就是WSGI協(xié)議的約定窘问,它有一個(gè)可調(diào)用的start_response 。environ包含了所有進(jìn)來(lái)的信息宜咒。 start_response用來(lái)表明已經(jīng)收到一個(gè)響應(yīng)惠赫。

通過(guò)Werkzeug,我們可以不必直接處理請(qǐng)求或者響應(yīng)這些底層的東西故黑,它已經(jīng)為我們封裝好了這些儿咱。

請(qǐng)求數(shù)據(jù)需要environ對(duì)象庭砍,Werkzeug允許我們以一個(gè)輕松的方式訪問(wèn)數(shù)據(jù)。響應(yīng)對(duì)象是一個(gè)WSGI應(yīng)用混埠,提供了更好的方法來(lái)創(chuàng)建響應(yīng)怠缸。如下所示:

fromwerkzeug.wrappersimportResponsedefapplication(environ,start_response):response=Response('Hello World!',mimetype='text/plain')returnresponse(environ,start_response)

2.3 如何理解wsgi, Werkzeug, flask之間的關(guān)系

Flask是一個(gè)基于Python開(kāi)發(fā)并且依賴jinja2模板和Werkzeug

WSGI服務(wù)的一個(gè)微型框架,對(duì)于Werkzeug钳宪,它只是工具包揭北,其用于接收http請(qǐng)求并對(duì)請(qǐng)求進(jìn)行預(yù)處理,然后觸發(fā)Flask框架吏颖,開(kāi)發(fā)人員基于Flask框架提供的功能對(duì)請(qǐng)求進(jìn)行相應(yīng)的處理搔体,并返回給用戶,如果要返回給用戶復(fù)雜的內(nèi)容時(shí)半醉,需要借助jinja2模板來(lái)實(shí)現(xiàn)對(duì)模板的處理疚俱。將模板和數(shù)據(jù)進(jìn)行渲染,將渲染后的字符串返回給用戶瀏覽器奉呛。

2.4 Flask是什么计螺,不是什么

Flask永遠(yuǎn)不會(huì)包含數(shù)據(jù)庫(kù)層,也不會(huì)有表單庫(kù)或是這個(gè)方面的其它東西瞧壮。Flask本身只是Werkzeug和Jinja2的之間的橋梁登馒,前者實(shí)現(xiàn)一個(gè)合適的WSGI應(yīng)用,后者處理模板咆槽。當(dāng)然陈轿,F(xiàn)lask也綁定了一些通用的標(biāo)準(zhǔn)庫(kù)包,比如logging秦忿。除此之外其它所有一切都交給擴(kuò)展來(lái)實(shí)現(xiàn)麦射。

為什么呢?因?yàn)槿藗冇胁煌钠煤托枨蟮埔ィ現(xiàn)lask不可能把所有的需求都囊括在核心里潜秋。大多數(shù)web應(yīng)用會(huì)需要一個(gè)模板引擎。然而不是每個(gè)應(yīng)用都需要一個(gè)SQL數(shù)據(jù)庫(kù)的胎许。

Flask 的理念是為所有應(yīng)用建立一個(gè)良好的基礎(chǔ)峻呛,其余的一切都取決于你自己或者 擴(kuò)展。

3. Flask源碼分析

Flask的使用非常簡(jiǎn)單辜窑,官網(wǎng)的例子如下:

fromflaskimportFlaskapp=Flask(__name__)@app.route("/")defhello():return"Hello World!"if__name__=="__main__":app.run()

每當(dāng)我們需要?jiǎng)?chuàng)建一個(gè)flask應(yīng)用時(shí)钩述,我們都會(huì)創(chuàng)建一個(gè)Flask對(duì)象:

app=Flask(__name__)

下面看一下Flask對(duì)象的__init__方法,如果不考慮jinjia2相關(guān)穆碎,核心成員就下面幾個(gè):

classFlask:def__init__(self,package_name):self.package_name=package_nameself.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()

我們把目光聚集到后面幾個(gè)成員牙勘,view_functions中保存了視圖函數(shù)(處理用戶請(qǐng)求的函數(shù),如上面的hello())所禀,error_handlers中保存了錯(cuò)誤處理函數(shù)方面,before_request_funcs和after_request_funcs保存了請(qǐng)求的預(yù)處理函數(shù)和后處理函數(shù)放钦。

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

defroute(...):defdecorator(f):self.add_url_rule(rule,f.__name__,**options)self.view_functions[f.__name__]=freturnfreturndecorator

上面說(shuō)到的是初始化部分最筒,下面看一下執(zhí)行部分,當(dāng)我們執(zhí)行app.run()時(shí)蔚叨,調(diào)用堆棧如下:

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

wsgi_app是flask核心:

defwsgi_app(self,environ,start_response):withself.request_context(environ):rv=self.preprocess_request()ifrvisNone:rv=self.dispatch_request()response=self.make_response(rv)response=self.process_response(response)returnresponse(environ,start_response)

可以看到床蜘,wsgi_app這個(gè)函數(shù)的作用就是先調(diào)用所有的預(yù)處理函數(shù),然后分發(fā)請(qǐng)求蔑水,再調(diào)用所有后處理函數(shù)邢锯,最后返回response。

看一下dispatch_request函數(shù)的實(shí)現(xiàn)搀别,因?yàn)榈で妫@里有flask的錯(cuò)誤處理邏輯:

defdispatch_request(self):try:endpoint,values=self.match_request()returnself.view_functions[endpoint](**values)exceptHTTPException,e:handler=self.error_handlers.get(e.code)ifhandlerisNone:returnereturnhandler(e)exceptException,e:handler=self.error_handlers.get(500)ifself.debugorhandlerisNone:raisereturnhandler(e)

如果出現(xiàn)錯(cuò)誤,則根據(jù)相應(yīng)的error code歇父,調(diào)用不同的錯(cuò)誤處理函數(shù)蒂培。

上面這段簡(jiǎn)單的源碼分析,就已經(jīng)將Flask幾個(gè)核心變量和核心函數(shù)串聯(lián)起來(lái)了榜苫。其實(shí)护戳,我們這里扣出來(lái)的幾段代碼,也就是Flask的核心代碼垂睬。畢竟媳荒,F(xiàn)lask的0.1版本包含大量注釋以后,也才六百行代碼驹饺。

4. flask的魔法

如果讀者打開(kāi)flask.py文件钳枕,將看到我前面的源碼分析幾乎已經(jīng)覆蓋了所有重要的代碼。但是赏壹,細(xì)心的讀者會(huì)看到鱼炒,在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開(kāi)發(fā)的魔法蝌借,也是flask源碼中的難點(diǎn)田柔。在分析之前,我們先看一下它們的作用骨望。

在flask的開(kāi)發(fā)過(guò)程中,我們可以通過(guò)如下方式訪問(wèn)url中的參數(shù):

fromflaskimportrequest@app.route('/')defhello():name=request.args.get('name',None)

看起來(lái)request像是一個(gè)全局變量欣舵,那么擎鸠,一個(gè)全局變量為什么可以在一個(gè)多線程環(huán)境中隨意使用呢,下面就隨我來(lái)一探究竟吧缘圈!

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

_request_ctx_stack=LocalStack()

正如它LocalStack()的名字所暗示的那樣劣光,_request_ctx_stack是一個(gè)棧袜蚕。顯然,一個(gè)椌钗校肯定會(huì)有push牲剃、pop和top函數(shù),如下所示:

classLocalStack(object):def__init__(self):self._local=Local()defpush(self,obj):rv=getattr(self._local,'stack',None)ifrvisNone:self._local.stack=rv=[]rv.append(obj)returnrvdefpop(self):stack=getattr(self._local,'stack',None)ifstackisNone:returnNoneeliflen(stack)==1:release_local(self._local)returnstack[-1]else:returnstack.pop()

按照我們的理解雄可,要實(shí)現(xiàn)一個(gè)棧凿傅,那么LocalStack類應(yīng)該有一個(gè)成員變量,是一個(gè)list数苫,然后通過(guò)這個(gè)list來(lái)保存棧的元素聪舒。然而,LocalStack并沒(méi)有一個(gè)類型是list的成員變量虐急,LocalStack僅有一個(gè)成員變量self._local = Local()箱残。

順藤摸瓜,我們來(lái)到了Werkzeug的源碼中止吁,到達(dá)了Local類的定義處:

classLocal(object):def__init__(self):object.__setattr__(self,'__storage__',{})object.__setattr__(self,'__ident_func__',get_ident)def__getattr__(self,name):try:returnself.__storage__[self.__ident_func__()][name]exceptKeyError:raiseAttributeError(name)def__setattr__(self,name,value):ident=self.__ident_func__()storage=self.__storage__try:storage[ident][name]=valueexceptKeyError:storage[ident]={name:value}

需要注意的是被辑,Local類有兩個(gè)成員變量,分別是__storage__和__ident_func__敬惦,其中盼理,前者是一個(gè)字典,后者是一個(gè)函數(shù)仁热。這個(gè)函數(shù)的含義是榜揖,獲取當(dāng)前線程的id(或協(xié)程的id)。

此外抗蠢,我們注意到举哟,Local類自定義了__getattr__和__setattr__這兩個(gè)方法,也就是說(shuō)迅矛,我們?cè)诓僮鱯elf.local.stack時(shí)妨猩,會(huì)調(diào)用__setattr__和__getattr__方法。

_request_ctx_stack=LocalStack()_request_ctx_stack.push(item)# 注意秽褒,這里賦值的時(shí)候壶硅,會(huì)調(diào)用__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]=valueexceptKeyError:storage[ident]={name:value}

在__setattr__中,通過(guò)__ident_func__獲取到了一個(gè)key销斟,然后進(jìn)行賦值庐椒。自此,我們可以知道蚂踊,LocalStack是一個(gè)全局字典约谈,或者說(shuō)是一個(gè)名字空間。這個(gè)名字空間是所有線程共享的。當(dāng)我們?cè)L問(wèn)字典中的某個(gè)元素的時(shí)候棱诱,會(huì)通過(guò)__getattr__進(jìn)行訪問(wèn)泼橘,__getattr__先通過(guò)線程id,找當(dāng)前這個(gè)線程的數(shù)據(jù)迈勋,然后進(jìn)行訪問(wèn)炬灭。

字段的內(nèi)容如下:

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

最后,我們來(lái)看一下其他幾個(gè)全局變量:

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僅僅是一個(gè)代理(可以想象設(shè)計(jì)模式中的代理模式)重归。

通過(guò)LocalStack和LocalProxy這樣的Python魔法,每個(gè)線程訪問(wèn)當(dāng)前請(qǐng)求中的數(shù)據(jù)(request, session)時(shí)镰官,

都好像都在訪問(wèn)一個(gè)全局變量提前,但是,互相之間又互不影響泳唠。這就是Flask為我們提供的便利狈网,也是我們

選擇Flask的理由!

5. 總結(jié)

在這篇文章中笨腥,我們簡(jiǎn)單地介紹了WSGI, jinjia2和Werkzeug拓哺,詳細(xì)介紹了Flask在web開(kāi)發(fā)中所處的位置和發(fā)揮的作用。最后脖母,深入Flask的源碼士鸥,了解了Flask的實(shí)現(xiàn)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末谆级,一起剝皮案震驚了整個(gè)濱河市烤礁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌肥照,老刑警劉巖脚仔,帶你破解...
    沈念sama閱讀 221,198評(píng)論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異舆绎,居然都是意外死亡鲤脏,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門(mén)吕朵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)猎醇,“玉大人,你說(shuō)我怎么就攤上這事努溃×蛩唬” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 167,643評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵梧税,是天一觀的道長(zhǎng)音半。 經(jīng)常有香客問(wèn)我则拷,道長(zhǎng),這世上最難降的妖魔是什么曹鸠? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,495評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮斥铺,結(jié)果婚禮上彻桃,老公的妹妹穿的比我還像新娘。我一直安慰自己晾蜘,他們只是感情好邻眷,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著剔交,像睡著了一般肆饶。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上岖常,一...
    開(kāi)封第一講書(shū)人閱讀 52,156評(píng)論 1 308
  • 那天驯镊,我揣著相機(jī)與錄音,去河邊找鬼竭鞍。 笑死板惑,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的偎快。 我是一名探鬼主播冯乘,決...
    沈念sama閱讀 40,743評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼晒夹!你這毒婦竟也來(lái)了裆馒?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,659評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤丐怯,失蹤者是張志新(化名)和其女友劉穎喷好,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體响逢,經(jīng)...
    沈念sama閱讀 46,200評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡绒窑,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了舔亭。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片些膨。...
    茶點(diǎn)故事閱讀 40,424評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖钦铺,靈堂內(nèi)的尸體忽然破棺而出订雾,到底是詐尸還是另有隱情,我是刑警寧澤矛洞,帶...
    沈念sama閱讀 36,107評(píng)論 5 349
  • 正文 年R本政府宣布洼哎,位于F島的核電站烫映,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏噩峦。R本人自食惡果不足惜锭沟,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望识补。 院中可真熱鬧族淮,春花似錦、人聲如沸凭涂。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,264評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)切油。三九已至蝙斜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間澎胡,已是汗流浹背孕荠。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,390評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留滤馍,地道東北人岛琼。 一個(gè)月前我還...
    沈念sama閱讀 48,798評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像巢株,于是被迫代替她去往敵國(guó)和親槐瑞。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

  • [TOC]一直想做源碼閱讀這件事阁苞,總感覺(jué)難度太高時(shí)間太少困檩,可望不可見(jiàn)。最近正好時(shí)間充裕那槽,決定試試做一下悼沿,并記錄一下...
    何柯君閱讀 7,191評(píng)論 3 98
  • Flask框架中有許多魔法將Web應(yīng)用開(kāi)發(fā)者與一些細(xì)節(jié)隔離開(kāi)來(lái),其中Context機(jī)制又是有別于其他框架的骚灸,這個(gè)機(jī)...
    靡不有初LB閱讀 1,986評(píng)論 2 5
  • 22年12月更新:個(gè)人網(wǎng)站關(guān)停糟趾,如果仍舊對(duì)舊教程有興趣參考 Github 的markdown內(nèi)容[https://...
    tangyefei閱讀 35,186評(píng)論 22 257
  • wsgi協(xié)議 關(guān)于wsgi協(xié)議就不贅述了,以下是最簡(jiǎn)單的符合wsgi的應(yīng)用 app attriabute app....
    __XY__閱讀 228評(píng)論 0 0
  • 上下文這個(gè)概念多見(jiàn)于文章中甚牲,是一句話中的語(yǔ)境义郑,也就是語(yǔ)言環(huán)境。一句莫名其妙的話出現(xiàn)會(huì)讓人不理解什么意思丈钙,如果有語(yǔ)言...
    饅頭白啊白閱讀 31,413評(píng)論 6 66