Flask源碼解析

兩個核心依賴

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魏滚。


image

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ù):

  1. 生成request請求對象和請求上下文(封裝在request_context函數(shù)里)
  2. 將生成的請求上下文(本次請求的環(huán)境)push入棧侵浸,存儲旺韭。
  3. 請求進入預處理(例如before_request),錯誤處理及請求轉發(fā)到響應的過程(full_dispatch_request函數(shù))

詳情查看:

https://blog.csdn.net/bestallen/article/details/54342120

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

所以總結流程就是:

  1. preprocess_request函數(shù)執(zhí)行預處理(例before_request)
  2. 若相關預處理函數(shù)出現(xiàn)返回值垃帅,提前結束
  3. 若正常執(zhí)行完所有預處理函數(shù)延届,無返回值
  4. 調用dispatch_request,執(zhí)行視圖函數(shù)贸诚,將結果封裝成rv
  5. 將視圖函數(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

由源碼可見

  1. LocalStack在init中創(chuàng)建了一個Local對象荠呐,此時storage是一個空字典
  2. 當調用push時赛蔫,即傳入線程或進程對象時,先判斷是否已存在泥张,否則新創(chuàng)建一個空間(列表呵恢,作為棧),入棧
  3. 當調用top時,返回棧頂元素
  4. 調用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哪來的

  1. 首先當請求進入時,__call__調用wsgi_app
  2. 在wsgi_app中初始化了一個請求上下文 ctx = self.request_context(environ)
  3. 可見是將environ作為參數(shù)傳入始绍,而在WSGI中規(guī)定 environ即保存著請求相關的數(shù)據(jù)
  4. 進入request_context() 函數(shù) 發(fā)現(xiàn)只有一行代碼 return RequestContext(self, environ)
  5. 進入RequestContext類 看到init函數(shù)中 request = app.request_class(environ)
  6. 通過以上 封裝了一個request對象.提供我們可以使用 request.method request.args等操作

session相關原理

通過源碼可以看到session的繼承中瞳购,存在dict。則session具備dict的所有操作亏推。

class SecureCookieSession(CallbackDict, SessionMixin):
                ↓
class CallbackDict(UpdateDictMixin, dict):
  1. session數(shù)據(jù)保存到redis
  2. 生成一個隨機字符串
  3. 返回一個隨機字符串給用戶学赛,并作為key
  4. 客戶端再訪問時返回該隨機字符串

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

上下文與棧

棧到底是怎么工作的

image

主要通過棧實現(xiàn)碴萧,即當一個請求進入時:

  1. 實例化一個requestcontext,封裝了本次請求的相關信息(在Request中)
  2. 在請求上下文入棧之前末购,先檢查應用上下文棧(源碼可見棧名為:_app_ctx_stack)是否為空破喻,為空則將當前app push()入棧
  3. 將請求上下文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)》
  • 各類視頻資料...
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末集漾,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子砸脊,更是在濱河造成了極大的恐慌具篇,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件凌埂,死亡現(xiàn)場離奇詭異驱显,居然都是意外死亡,警方通過查閱死者的電腦和手機瞳抓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門埃疫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人孩哑,你說我怎么就攤上這事栓霜。” “怎么了横蜒?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵胳蛮,是天一觀的道長。 經常有香客問我丛晌,道長仅炊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任茵乱,我火速辦了婚禮茂洒,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己督勺,他們只是感情好渠羞,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著智哀,像睡著了一般次询。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上瓷叫,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天屯吊,我揣著相機與錄音,去河邊找鬼摹菠。 笑死盒卸,一個胖子當著我的面吹牛,可吹牛的內容都是我干的次氨。 我是一名探鬼主播蔽介,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼煮寡!你這毒婦竟也來了虹蓄?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤幸撕,失蹤者是張志新(化名)和其女友劉穎薇组,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體坐儿,經...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡律胀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了挑童。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片累铅。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡跃须,死狀恐怖站叼,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情菇民,我是刑警寧澤尽楔,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站第练,受9級特大地震影響阔馋,放射性物質發(fā)生泄漏。R本人自食惡果不足惜娇掏,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一呕寝、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧婴梧,春花似錦下梢、人聲如沸客蹋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽讶坯。三九已至,卻和暖如春岗屏,著一層夾襖步出監(jiān)牢的瞬間辆琅,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工这刷, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留婉烟,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓暇屋,卻偏偏與公主長得像隅很,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子率碾,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內容