兩個核心依賴
falsk主要依賴兩個庫 —— Werkzeug 和 Jinja钾军。
Jinja2
由于大多數(shù)Web程序都需要渲染模板赊豌,與Jinja2集成可以減少大量的工作。此處不展開討論撞蚕。
Werkzeug
Flask的核心擴展就是Werkzeug蛙粘。
python Web框架都需要處理WSGI交互随闽,它是為了讓Web服務器與python程序能夠進行數(shù)據(jù)交流而定義的一套接口標準/規(guī)范父丰。而Werkzeug是一個優(yōu)秀的WSGI工具庫。
HTTP請求 -》 WSGI規(guī)定的數(shù)據(jù)格式 -》 Web程序
從路由處理掘宪,到請求解析蛾扇,再到響應封裝,以及上下文和各種數(shù)據(jù)結構都離不開Werkzeug魏滚。
WSGI程序
根據(jù)WSGI的規(guī)定镀首,Web程序(WSGI程序)必須是一個可調用對象。這個可調用對象接收兩個參數(shù):
- environ:包含了請求的所有信息的字典鼠次。
- start_response:需要在可調用對象中調用的函數(shù)更哄,用來發(fā)起響應,參數(shù)是狀態(tài)碼腥寇,響應頭部等
WSGI服務器會在調用這個可調用對象時傳入這兩個參數(shù)成翩。另外這個可調用對象還要返回一個可迭代對象。
這個可調用對象可以是函數(shù)赦役、方法麻敌、類或是實現(xiàn)了call方法的類實例。
以下借助簡單的實例來了解最主要的兩種實現(xiàn):函數(shù)和類
# 函數(shù)實現(xiàn)
# 可調用對象 接收兩個參數(shù)
def hello(environ, start_response):
# 響應信息
status = '200 OK'
response_headers = [('Content-type', 'text/html')]
# 需要在可調用函數(shù)中調用的函數(shù)
start_response(status, response_headers)
# 返回可迭代對象
return [b'<h1>Hello</h1>']
注:WSGI規(guī)定請求和響應主體應該為字符串(bytestrings)掂摔,即py2中的str术羔。在py3中字符串默認為unicode類型,因此需要在字符串前添加b聲明為bytes類型,兼容兩者
# 類實現(xiàn)
class AppClass:
def __init__(self, environ, start_response):
self.environ = environ
self.statr = start_response
# iter方法乙漓,這個類被迭代時级历,調用這個方法
# 實現(xiàn)該方法的類就是迭代器
def __iter__(self):
status = '200 OK'
response_headers = [('Content-type', 'text/html')]
self.start(status, response_headers)
yield b'<h1>Hello</h1>'
werkzeug中如何實現(xiàn)Web程序
由于flask是基于werkzeug實現(xiàn)的,所以先了解以下werkzeug是如何實現(xiàn)一個簡單的web程序
from werkzeug.wrappers import Request, Response
@Request.application
def hello(request):
return Response('hello')
if __name__ == '__main__':
from werkzeug.serving import run_simple
run_simple('localhost', 5000, hello)
通過以上代碼簇秒,使用run_simple規(guī)定了ip鱼喉、端口號秀鞭、調用對象
路由是怎么設定的趋观?
Werkzeug怎么實現(xiàn)路由系統(tǒng)
# 路由表
m = Map()
rule1 = Rule('/', endpoint='index')
rule2 = Rule('/downloads/', endpoint='downloads/index')
m.add(rule1)
m.add(rule2)
Flask的路由系統(tǒng)
Flask使用中的路由系統(tǒng)扛禽,是通過route() 裝飾器來將視圖函數(shù)注冊為路由。進入route函數(shù)
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
可見內部調用了add_url_rule皱坛,并將函數(shù)作為參數(shù)傳入编曼。看到add_url_rule存在關鍵的語句
# url_map實際上就是Map類的實例
# rule就是通過route相關更正成的Rule實例
self.url_map.add(rule)
# view_functions是一個字典剩辟,存儲了端點和視圖函數(shù)的映射關系掐场。可用于查詢
self.view_functions[endpoint] = view_func
再進入底層就會發(fā)現(xiàn)贩猎,實際上就同上例的werkzeug實現(xiàn)
導入config配置參數(shù)
最初熊户,我們修改配置文件會使用以下方法
app.config['DEGUB'] = True
導入?yún)?shù)
import config
app.config.from_object(config)
# 在config.py 文件中 存放配置參數(shù)
DEBUG = True
SECRET_KEY = os.urandom(24)
DIALECT = 'mysql'
DRIVER = 'mysqlconnector'
USERNAME = 'root'
PASSWORD = 'root'
HOST = '127.0.0.1'
PORT = '3306'
DATABASE = 'test'
如果自定義了配置文件類也可傳入字符串
app.config.from_object('config.Foo')
# 以上代表 config.py文件中的 Foo類
進入from_object() 函數(shù) [位于config.py]
def from_object(self, obj):
if isinstance(obj, string_types):
obj = import_string(obj)
for key in dir(obj):
if key.isupper():
self[key] = getattr(obj, key)
首先判斷如果是字符串類型的,做相應處理獲得對象吭服。在import_string函數(shù)中
module_name, obj_name = import_name.rsplit(".", 1)
module = __import__(module_name, globals(), locals(), [obj_name])
dir()函數(shù)的作用:
dir() 函數(shù)不帶參數(shù)時嚷堡,返回當前范圍內的變量、方法和定義的類型列表艇棕;
帶參數(shù)時蝌戒,返回參數(shù)的屬性、方法列表沼琉。
如果參數(shù)包含方法__dir__()北苟,該方法將被調用。
如果參數(shù)不包含__dir__()打瘪,該方法將最大限度地收集參數(shù)信息友鼻。
獲取屬性后判斷是否為大寫,是則添加為配置參數(shù)
用類導入配置的作用
在開發(fā)和線上瑟慈,往往采用的不是相同的配置文件桃移。我們可以通過類封裝幾套配置文件以供使用。
可以編寫一個基礎類葛碧,在開發(fā)測試借杰、線上運行都相同、都需要的配置參數(shù)进泼。再通過繼承蔗衡,擴展不同環(huán)境下的不同配置參數(shù)。
則在不同的環(huán)境下乳绕,只需要改變from_object() 中的參數(shù)即可绞惦。
Flask如何處理請求
app程序對象
在一些Python web框架中,視圖函數(shù)類似
@route('/')
def index():
return 'hello'
但在flask中
@app.route('/')
def index():
return 'hello'
flask 中存在一個顯式的程序對象洋措,我們需要在全局空間中創(chuàng)建它济蝉。設計原因主要包括:
- 相較于隱式程序對象,同一時間只能有一個實例存在,顯式的程序對象允許多個程序實例存在王滤。
- 允許通過子類化Flask類來改變程序行為贺嫂。
- 允許通過工廠函數(shù)來創(chuàng)建程序實例,可以在不同的地方傳入不同的配置來創(chuàng)建不同的程序實例雁乡。
- 允許通過藍本來模塊化程序第喳。
啟動app.run()
在Flask類中
當調用app.run(),程序啟動踱稍。我們查看run()函數(shù)的源碼
from werkzeug.serving import run_simple
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
可見run_simple函數(shù)曲饱,而第三個參數(shù)是self,即flask對象珠月。
當調用對象時扩淀,python會執(zhí)行__call__
方法。
進入Flask() 類可以看到
def wsgi_app(self, environ, start_response):
ctx = self.request_context(environ)
error = None
try:
try:
ctx.push()
response = self.full_dispatch_request()
except Exception as e:
error = e
response = self.handle_exception(e)
except: # noqa: B001
error = sys.exc_info()[1]
raise
return response(environ, start_response)
finally:
if self.should_ignore_error(error):
error = None
ctx.auto_pop(error)
def __call__(self, environ, start_response):
"""The WSGI server calls the Flask application object as the
WSGI application. This calls :meth:`wsgi_app` which can be
wrapped to applying middleware."""
return self.wsgi_app(environ, start_response)
當請求到來時啤挎,程序在調用app時引矩,由于實現(xiàn)了__call__
函數(shù),則通過該函數(shù)調用了wsgi_app()函數(shù)
具體分析wsgi_app函數(shù):
- 生成request請求對象和請求上下文(封裝在request_context函數(shù)里)
- 將生成的請求上下文(本次請求的環(huán)境)push入棧侵浸,存儲旺韭。
- 請求進入預處理(例如before_request),錯誤處理及請求轉發(fā)到響應的過程(full_dispatch_request函數(shù))
詳情查看:
before_request\after_request
在平常使用中掏觉,我們還會使用裝飾器before_request對某些請求執(zhí)行前做一些相關操作区端。
我們進入before_request源碼中,可以看到實際上就一行代碼
def before_request(self, f):
self.before_request_funcs.setdefault(None, []).append(f)
return f
并且從源碼中可以看到before_request_funcs只是Flask類中初始化的一個空字典澳腹。所以以上函數(shù)就是將字典設置為
{
None : [func1, func2...]
}
鍵為none织盼,值為存儲了before_request函數(shù)的列表
回頭再看到當請求到達時,__call__
調用wsgi_aqq函數(shù)
# 先是將請求相關的資源環(huán)境封裝成請求上下文對象 并入棧
ctx = self.request_context(environ)
error = None
try:
try:
ctx.push()
response = self.full_dispatch_request()
進入full_dispatch_request
try:
request_started.send(self)
rv = self.preprocess_request()
if rv is None:
rv = self.dispatch_request()
再進入preprocess_request
bp = _request_ctx_stack.top.request.blueprint
funcs = self.url_value_preprocessors.get(None, ())
if bp is not None and bp in self.url_value_preprocessors:
funcs = chain(funcs, self.url_value_preprocessors[bp])
for func in funcs:
func(request.endpoint, request.view_args)
funcs = self.before_request_funcs.get(None, ())
if bp is not None and bp in self.before_request_funcs:
funcs = chain(funcs, self.before_request_funcs[bp])
for func in funcs:
rv = func()
if rv is not None:
return rv
看到后半部分酱塔,實際上就是把剛剛字典(before_request_funcs)中的的函數(shù)遍歷出來執(zhí)行沥邻。如果存在返回值,則直接返回羊娃。
所有如果當前的before_request函數(shù)存在并且返回了值唐全,則之后的函數(shù)before_request函數(shù)后不會被執(zhí)行,并且視圖函數(shù)也不會執(zhí)行蕊玷,可見調用before_request的源碼(前文已提到)
rv = self.preprocess_request()
# 若不存在返回值邮利, 才執(zhí)行視圖函數(shù)
if rv is None:
rv = self.dispatch_request()
# 否則處理錯誤
except Exception as e:
rv = self.handle_user_exception(e)
# 執(zhí)行后處理 生成最終的response
return self.finalize_request(rv)
再看一下finalize_request
def finalize_request(self, rv, from_error_handler=False):
'''
把視圖函數(shù)返回值轉換為響應,然后調用后處理函數(shù)
'''
response = self.make_response(rv) # 生成響應
try:
response = self.process_response(response) # 響應預處理
request_finished.send(self, response=response) # 發(fā)送信號
except Exception:
if not from_error_handler:
raise
self.logger.exception(
"Request finalizing failed with an error while handling an error"
)
return response
所以總結流程就是:
- preprocess_request函數(shù)執(zhí)行預處理(例before_request)
- 若相關預處理函數(shù)出現(xiàn)返回值垃帅,提前結束
- 若正常執(zhí)行完所有預處理函數(shù)延届,無返回值
- 調用dispatch_request,執(zhí)行視圖函數(shù)贸诚,將結果封裝成rv
- 將視圖函數(shù)生成的返回值rv傳遞給finalize_request方庭,生成響應對象并且執(zhí)行后處理
整理flask請求進入的邏輯
wsgi ( run_simple函數(shù)等待請求到來)
↓
調用flask的 __call__ ( 由于run_simple的self參數(shù))
↓
__call__ 返回調用 wsgi_app()
→ ctx = self.request_context(environ) 把請求相關信息傳入初始化得一個ctx對象(請求上下文)
ctx.push() 將上下文對象入棧(localStack) → Local存儲(維護__storage__ = {122:{stack:[ctx,]}})
↓
視圖函數(shù)從localStack(再從local)中取出上下文進行操作
[圖片上傳失敗...(image-171246-1565862389864)]
關于Local
通過上述關系厕吉,可知local是作為一個動態(tài)的存儲倉庫。通過線程/進程id設置其運行環(huán)境(上下文)械念。
進入Local()類中 【local.py】
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}
可以看到init函數(shù)中 調用了object類的setattr赴涵。但實際上本類中也存在,甚至可以不使用setattr订讼,直接用賦值語句 __storage__
= {}也可。那為什么要調用父類的setattr呢扇苞。
回到Local的作用:動態(tài)的存儲運行環(huán)境欺殿。
Local采用__storage__作為倉庫存儲
那么面臨兩個問題:
1. 初始化__storage__
2. 動態(tài)賦值(格式為__storage__ :{122:{stack:[ctx,]}})
解決動態(tài)賦值問題,即重寫賦值函數(shù)(賦值語句的實質就是調用__setattr__)
從源碼中可以看到Local類重寫了__setattr__函數(shù)鳖敷,實現(xiàn)了所需的要求
那么此時該如何初始化__storage__呢
由于我們新重寫的setattr函數(shù)中調用了storage脖苏,但未初始化之前就使用了它,明顯錯誤
于是使用object的setattr函數(shù)來初始化storage定踱,就完美的解決了以上問題棍潘。
關于LocalStack
注:在local中 __storage__
的實質是字典,它的val也是字典(不同進程線程的存儲空間)崖媚,val的key名為stack(源碼規(guī)定)亦歉, val的val是列表(用棧實現(xiàn))(用于管理上下文)
在單次請求中,我們真正要使用的是當前環(huán)境下的上下文畅哑,所以如果只依靠Local:
obj = Local()
obj.stack = []
obj.stack.append(上下文環(huán)境)
顯然不易于維護肴楷、可擴展性差
于是使用LocalStack作為代理。查看源碼LocalStack()類 (local.py
)
class LocalStack(object):
def __init__(self):
self._local = Local()
def push(self, obj):
"""Pushes a new item to the stack"""
rv = getattr(self._local, "stack", None)
if rv is None:
self._local.stack = rv = []
rv.append(obj)
return rv
def pop(self):
"""Removes the topmost item from the stack, will return the
old value or `None` if the stack was already empty.
"""
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):
try:
return self._local.stack[-1]
except (AttributeError, IndexError):
return None
由源碼可見
- LocalStack在init中創(chuàng)建了一個Local對象荠呐,此時storage是一個空字典
- 當調用push時赛蔫,即傳入線程或進程對象時,先判斷是否已存在泥张,否則新創(chuàng)建一個空間(列表呵恢,作為棧),入棧
- 當調用top時,返回棧頂元素
- 調用pop時若棧中只剩一個元素逃魄,則取出后刪除該椥空間,否則pop棧頂元素
在上下文之前
在解釋上下文之前晌姚,先看看上下文和以上的棧有什么聯(lián)系
通過以上實現(xiàn)的棧,我們做出以下假設歇竟,用上下文存儲當前請求的環(huán)境(包括request信息挥唠、session等)
# 請求上下文
class RequestContext(object):
def __init__(self):
self.request = "xx"
self.session = "oo"
# 初始化一個存儲棧空間
xxx = LocalStack()
# 當請求進入時焕议,初始化一個請求上下文宝磨、封裝了當前環(huán)境
ctx = RequestContext()
# 將該請求上下文入棧
xxx.push(ctx)
# 當需要使用相關資源時弧关,取當前棧頂元素,即可操作相關數(shù)據(jù)
obj = xxx.top()
obj.request
obj.session
具體源碼下章解析
本地上下文
以上所談及的上下文究竟是什么呢唤锉?
在多線程環(huán)境下世囊,要想讓所有視圖函數(shù)都獲取請求對象。
- 最直接的方法就是在調用視圖函數(shù)時將所有需要的數(shù)據(jù)作為參數(shù)傳遞進去窿祥,但這樣一來程序邏輯就變得冗余不易于維護株憾。
- 另一種方法是將這些數(shù)據(jù)設為全局變量,但是這樣必然會在不同的線程中出現(xiàn)混亂(非線程安全)晒衩。
本地線程(thread locals) 的出現(xiàn)解決了這些問題嗤瞎。
本地線程就是一個全局對象,使用一種特定線程且線程安全的方式來存儲和獲取數(shù)據(jù)听系。也就是說贝奇,同一個變量在不同線程內擁有各自的值,互不干擾靠胜。
實現(xiàn)原理其實很簡單掉瞳,就是根據(jù)線程的ID來存取數(shù)據(jù)。
Flask沒有使用標準庫的threading.local()浪漠,而是使用了Werkzeug自己實現(xiàn)的本地線程對象werkzeug.local.Local()陕习,后者增加了對Greenlet(以C擴展形式接入python的輕量級協(xié)程)的優(yōu)先支持。
Flask使用本地線程來讓上下文代理對象全局可訪問址愿,比如:
- request
- session
- current_app
- g
這些對象被稱為本地上下文對象(context locals)衡查。
所以,在不基于線程必盖、greenlet或單進程實現(xiàn)的并發(fā)服務器上拌牲,這些代理對象將無法正常工作,但僅有少部分服務器不支持歌粥。
Flask的設計初衷是為了讓傳統(tǒng)Web程序開發(fā)更加簡單和迅速塌忽,二不是用來開發(fā)大型程序或異步服務器的。但Flask 的可擴展性卻提供了無限的可能性失驶,除了使用擴展土居,還可以子類化Flask類或為程序添加中間件。
應用上下文嬉探、請求上下文都是對象擦耀,是對一系列flask對象的封裝,并且提供相關的接口方法涩堤。
- 請求上下文: request session
- 應用上下文: app g
- flask中上下文相關的代碼存放在
ctx.py
請求上下文
請求上下文最主要的是提供對Request請求對象的封裝眷蜓。
RequestContext(object) // 請求上下文
- __init__
- push
- pop
- __enter__
- __exit__
先看源碼中init函數(shù)的作用
def __init__(self, app, environ, request=None, session=None):
self.app = app
if request is None:
request = app.request_class(environ)
self.request = request
self.url_adapter = None
try:
self.url_adapter = app.create_url_adapter(self.request)
except HTTPException as e:
self.request.routing_exception = e
self.flashes = None
self.session = session
可以看到就是對當前請求相關數(shù)據(jù)的初始化,如 當前app對象胎围、request吁系、session德召、flashes等,符合上章所提到的上下文和棧的關系作用汽纤。
認識
請求到來時:
# self是app對象上岗,environ是請求相關的原始數(shù)據(jù)(根據(jù)WSGI規(guī)定)
ctx = RequestContext(self, environ)
ctx.request = Request(environ)
ctx.session = None
# 不同的線程在內部分別持有不同的資源
{
1232:{ctx: ctx對象}
1231:{ctx: ctx對象}
2141:{ctx: ctx對象}
1235:{ctx: ctx對象}
}
視圖函數(shù):
from flask import request,session
# falsk 自動的識別當前線程,找到對應的ctx里的request蕴坪、session
請求結束:
根據(jù)當前線程的唯一標記肴掷,將數(shù)據(jù)資源移除
實現(xiàn)
flask利用local()為線程或協(xié)程開辟資源空間,并用stack【棻炒】存儲維護呆瞻,內部再使用偏函數(shù)【functools.partial(func1, 10)】拆分各屬性值。
app.run()
0. wsgi(處理請求续室,準備調用__call__)
1. app.__call__(準備調用wsgi_app)
2. app.wsgi_app(準備實例化RequestContext)
3. ctx = RequestContext(session, request)
- 請求相關+空session 封裝到RequestContext(ctx)
4. ctx.push()
- 將ctx交給LocalStack對象
5. LocalStack,把ctx對象添加到local中
- LocalStack相當于將單個線程或協(xié)程的數(shù)據(jù)資源分割開來,并作為棧進行維護
6. Local __storage__ = {
1231: {stack: [ctx(request, session), ]}
}
- local的結構谒养。存儲了多個線程或協(xié)程的資源數(shù)據(jù)
7. session存儲
根據(jù)請求中的cookie提取名為sessionid對應的值挺狰,對cookie加密+反序列化,再賦值給ctx里的session
8. 視圖函數(shù)
- 利用flask已經封裝好的庫买窟,調用session或request的相關資源
9. 操作結束后
把session中的數(shù)據(jù)再次寫入cookie中丰泊,將ctx刪除
10. 結果返回給用戶瀏覽器
11. 斷開socket連接
request哪來的
- 首先當請求進入時,
__call__
調用wsgi_app - 在wsgi_app中初始化了一個請求上下文 ctx = self.request_context(environ)
- 可見是將environ作為參數(shù)傳入始绍,而在WSGI中規(guī)定 environ即保存著請求相關的數(shù)據(jù)
- 進入request_context() 函數(shù) 發(fā)現(xiàn)只有一行代碼 return RequestContext(self, environ)
- 進入RequestContext類 看到init函數(shù)中 request = app.request_class(environ)
- 通過以上 封裝了一個request對象.提供我們可以使用 request.method request.args等操作
session相關原理
通過源碼可以看到session的繼承中瞳购,存在dict。則session具備dict的所有操作亏推。
class SecureCookieSession(CallbackDict, SessionMixin):
↓
class CallbackDict(UpdateDictMixin, dict):
- session數(shù)據(jù)保存到redis
- 生成一個隨機字符串
- 返回一個隨機字符串給用戶学赛,并作為key
- 客戶端再訪問時返回該隨機字符串
flash
flask中存在消息閃現(xiàn)機制,通過flash()源碼(helpers.py)可以看到吞杭,本質上是利用session實現(xiàn)的
# category表示消息的類別盏浇,可以按類別存入,按類別彈出
def flash(message, category="message"):
flashes = session.get("_flashes", [])
flashes.append((category, message))
session["_flashes"] = flashes
message_flashed.send(
current_app._get_current_object(), message=message, category=category
)
彈出flash信息函數(shù)
def get_flashed_messages(with_categories=False, category_filter=()):
flashes = _request_ctx_stack.top.flashes
if flashes is None:
_request_ctx_stack.top.flashes = flashes = (
session.pop("_flashes") if "_flashes" in session else []
)
if category_filter:
flashes = list(filter(lambda f: f[0] in category_filter, flashes))
if not with_categories:
return [x[1] for x in flashes]
return flashes
則最終實現(xiàn)的效果是 flash() 存入信息芽狗,get_flashed_messages()只能對應的彈出一次绢掰。
應用上下文
應用上下文最主要的就是提供對核心對象flask的封裝。
源代碼中類的主要結構為:
AppContext(object) // 應用上下文
- push
- pop
- __enter__
- __exit__
g
每個請求進入時童擎,都會創(chuàng)建一個g滴劲,一次完整請求為一個生命周期。
當多線程進入時顾复,由于g的唯一標識為線程(Local中的__storage__
)班挖,所以資源互不影響⌒驹遥可以使用g為每次請求設置一個值聪姿。
# 例:
@app.before_request
def x1():
g.x1 = 123
@app.route('/index')
def index():
print(g.x1)
return "index"
current_app
上下文與棧
棧到底是怎么工作的
主要通過棧實現(xiàn)碴萧,即當一個請求進入時:
- 實例化一個requestcontext,封裝了本次請求的相關信息(在Request中)
- 在請求上下文入棧之前末购,先檢查應用上下文棧(源碼可見棧名為:_app_ctx_stack)是否為空破喻,為空則將當前app push()入棧
- 將請求上下文push()入棧(源碼可見棧名為:_request_ctx_stack)
# RequestContext類中
# 可以看到先判斷app_ctx是否存在,然后再push入棧request_ctx
app_ctx = _app_ctx_stack.top
if app_ctx is None or app_ctx.app != self.app:
# app_context 用于創(chuàng)建app_ctx對象
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)
_request_ctx_stack.push(self)
由于以上判斷盟榴,所以我們在視圖函數(shù)中使用current_app時曹质,由于有請求上下文,所以不需要手動將應用上下文app_ctx入棧擎场。如果在視圖函數(shù)外羽德,沒有請求發(fā)生時,使用current_app則需要手動入棧
app_ctx = app.app_context()
app_ctx.push()
# 可使用current_app
app_ctx.pop()
何時會用到迅办?
在實際生產中宅静,current_app對象一般都是至與視圖函數(shù)中使用
由于有正在的請求到來,所以不需要手動入棧站欺。
但是在代碼測試階段姨夹,在進行單元測試時,或離線應用(不使用postman等工具發(fā)生完整請求)
沒有實際的請求到來矾策,又需要對代碼進行測試
則需要手動將app_ctx入棧
with優(yōu)化出入棧
# with優(yōu)化 不需要手動push pop
with app.app_context():
# __enter__(連接)
a = current_app
d = current_app.config['DEBUG']
# __exit__(釋放連接【資源】)
# (__exit__內部實現(xiàn)了異常處理磷账,若成功處理了返回True,若沒有成功處理贾虽,返回False還會向外部拋出異常)
# 出了with環(huán)境 app對象被pop()出棧 current_app 就找不到目標了
# with可以對實現(xiàn)了上下文協(xié)議的對象使用
# 上下文管理器(app context)
# 實現(xiàn)了__enter__(連接) __exit__(釋放連接【資源】)就是上下文管理器
# 上下文表達式必須要返回一個上下文管理器
# 此時a是__enter__ 的返回值
with app.app_context() as a:
pass
# 可以自己實現(xiàn)上下文管理器逃糟,必須實現(xiàn)__enter__ __exit__方法
class MyResource:
def __enter__(self):
print('connect to resource')
# 將管理器返回再利用管理器進行相關操作
return self
def __exit__(self,exc_type, exc_value, tb):
print('close connection')
return True/False
# 返回True 表明此若產生異常內部進行處理,外部不會接收到異常
def query(self):
print('doing')
with MyResource() as r:
r.query()
# 也可以通過裝飾器,省略__enter__ __exit__ (不推薦)
from contextlib import contextmanager
class MyResource:
def query(self):
print('doing')
@contextmanager
def make_myresource():
print('connect to resource')
# yield做返回蓬豁,使用結束后再回到函數(shù)關閉連接
yield MyResource()
print('close connection')
with MyResource() as r:
r.query()
# 但是更好的做法是將本身不是上下文管理器的類绰咽,變?yōu)樯舷挛墓芾砥?# 例:輸入書名 with中自動添加 《》
# 操作數(shù)據(jù)庫 with中自動連接、回滾地粪、斷開
源碼中的體現(xiàn)
從源碼中可以看到無論是應用上下文還是請求上下文剃诅,都具有以下兩個函數(shù)
def __enter__(self):
self.push()
return self
def __exit__(self, exc_type, exc_value, tb):
# do not pop the request stack if we are in debug mode and an
# exception happened. This will allow the debugger to still
# access the request object in the interactive shell. Furthermore
# the context can be force kept alive for the test client.
# See flask.testing for how this works.
self.auto_pop(exc_value)
if BROKEN_PYPY_CTXMGR_EXIT and exc_type is not None:
reraise(exc_type, exc_value, tb)
即在進入時將上下文入棧,使用完畢后自動pop出棧
棧中的元素
從源碼中可以看到驶忌,push()的是上下文對象矛辕,但是我們真正使用的并非是上下文,而是current_app\request 等對象
源碼中
current_app = LocalProxy(_find_app)
再看_find_app
def _find_app():
top = _app_ctx_stack.top
if top is None:
raise RuntimeError(_app_ctx_err_msg)
return top.app
注意到current_app是取app_ctx_stack的棧頂元素的app對象
同理request付魔、g聊品、session
def _lookup_req_object(name):
top = _request_ctx_stack.top
if top is None:
raise RuntimeError(_request_ctx_err_msg)
return getattr(top, name)
LocalProxy代理
代理有什么用?
所有的數(shù)據(jù)都存儲在Local中几苍,如果直接對數(shù)據(jù)進行存取翻屈,需要建立多個類進行對數(shù)據(jù)的存取。如request類妻坝、session類伸眶、g類惊窖、current_app類。
但是由于以上類的功能相同厘贼,可以抽象出來界酒,使用一個代理類,完成所需功能嘴秸。
知識預備
# 偏函數(shù)
import functools
def index(a1, a2)
return a1 + a2
new_func = functools.partial(index, 666)
# 幫助自動傳遞參數(shù)
new_func(1) // 667
源碼體現(xiàn)
在我們實際運用中毁欣,并不是直接去操作上下文。而是使用例如:current_app\request\session\g等 通過源碼看到
_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"))
我們先進入LocalProxy類岳掐,看到init函數(shù)
def __init__(self, local, name=None):
object.__setattr__(self, "_LocalProxy__local", local)
object.__setattr__(self, "__name__", name)
if callable(local) and not hasattr(local, "__release_local__"):
# "local" is a callable that is not an instance of Local or
# LocalManager: mark it as a wrapped function.
object.__setattr__(self, "__wrapped__", local)
即為該對象設置值凭疮,而我們在實例化的時候,傳遞的參數(shù)是一個偏函數(shù)
那么當我們創(chuàng)建完代理對象后串述,考慮我們是怎樣使用這些代理的: request.method request.args等执解,則實際上會調用對象的getattr。進入源碼
def __getattr__(self, name):
if name == "__members__":
return dir(self._get_current_object())
return getattr(self._get_current_object(), name)
進入_get_current_object函數(shù)
def _get_current_object(self):
"""Return the current object. This is useful if you want the real
object behind the proxy at a time for performance reasons or because
you want to pass the object into a different context.
"""
if not hasattr(self.__local, "__release_local__"):
return self.__local()
try:
return getattr(self.__local, self.__name__)
except AttributeError:
raise RuntimeError("no object bound to %s" % self.__name__)
而local()實際上就是我們傳遞進來的偏函數(shù)(init()初始化的結果)
回頭看一下傳遞進來的偏函數(shù)纲酗,看到源碼中的_lookup_req_object
def _lookup_req_object(name):
top = _request_ctx_stack.top
if top is None:
raise RuntimeError(_request_ctx_err_msg)
return getattr(top, name)
即取出棧頂?shù)脑?上下文)衰腌,再通過getattr獲取到相關的內容。而erquest耕姊、session等桶唐,在前面也已經看到栅葡,是在上下文初始化時就創(chuàng)建的茉兰。所以該函數(shù)最終就是根據(jù)傳遞進來的參數(shù)(request, session, g, current_app),進入到local棧中欣簇,top拿到棧頂?shù)纳舷挛墓媪常缓笤谏舷挛闹腥〕鏊璧馁Y源。
三種程序狀態(tài)
Flask提供的四個本地上下文對象分別在特定的程序狀態(tài)下綁定實際的對象熊咽。如果我們在訪問或使用它們時還沒有綁定莫鸭,就會看到經典的RuntimeError異常。
Flask中存在三種狀態(tài):
- 程序設置狀態(tài)
- 程序運行狀態(tài)
- 請求運行狀態(tài)
程序設置狀態(tài)
當Flask類被實例化横殴,也就是創(chuàng)建程序實例app后被因,就進入程序設置狀態(tài)。這是所有的全局對象都沒有被綁定:
app = Flask(__name__)
程序運行狀態(tài)
當Flask程序啟動衫仑,但是還沒有請求進入時梨与,F(xiàn)lask進入了程序運行狀態(tài)。
在這種狀態(tài)下文狱,程序上下文對象current_app和g都綁定了各自的對象粥鞋。
使用flask shell命令打開的python shell默認就是這種狀態(tài),我們也在普通的Python shell中通過手動推送程序上下文來模擬:
app = Flask(__name__)
ctx = app.app_context()
ctx.push()
# current_app g /Flask flask.g
# requst session /unbound
以上我們手動使用app_context() 創(chuàng)建了程序上下文瞄崇,然后調用push() 方法把它推送到程序上下文堆棧里呻粹。
默認情況下壕曼,當請求進入的時候,程序上下文會隨著請求上下文一起被自動激活等浊。但是在沒有請求進入的場景腮郊,比較離線腳本、測試或者進行交互調試的時候凿掂,手動推送程序上下文以進入程序運行狀態(tài)會非常方便伴榔。
請求運行狀態(tài)
當請求進入的時候,或是使用test_request_context()方法庄萎、test_client()方法時踪少,F(xiàn)lask會進入請求運行狀態(tài)。因為當請求上下文被推送時糠涛,程序上下文會被自動推送援奢,所以在這個狀態(tài)下4個全局對象都會被綁定。我們可以通過手動推送請求上下文模擬:
app = Flask(__name__)
ctx = app.test_request_context()
ctx.push()
# current_app, g, request, session
# Flask flask.g Request NullSession
這也是為什么可以直接在視圖函數(shù)和相應的回調函數(shù)里直接使用這些上下文對象忍捡,而不用推送上下文(Flask在處理請求時會自動推送請求上下文和程序上下文)
引用
- 《Flask Web 開發(fā)實戰(zhàn)》
- 各類視頻資料...