Flask源碼解析

兩個核心依賴

falsk主要依賴兩個庫 —— Werkzeug 和 Jinja。

Jinja2

由于大多數Web程序都需要渲染模板,與Jinja2集成可以減少大量的工作。此處不展開討論。

Werkzeug

Flask的核心擴展就是Werkzeug。

python Web框架都需要處理WSGI交互,它是為了讓Web服務器與python程序能夠進行數據交流而定義的一套接口標準/規范。而Werkzeug是一個優秀的WSGI工具庫。

HTTP請求 -》 WSGI規定的數據格式 -》 Web程序

從路由處理,到請求解析,再到響應封裝,以及上下文和各種數據結構都離不開Werkzeug。


image

WSGI程序

根據WSGI的規定,Web程序(WSGI程序)必須是一個可調用對象。這個可調用對象接收兩個參數:

  • environ:包含了請求的所有信息的字典。
  • start_response:需要在可調用對象中調用的函數,用來發起響應,參數是狀態碼,響應頭部等

WSGI服務器會在調用這個可調用對象時傳入這兩個參數。另外這個可調用對象還要返回一個可迭代對象。

這個可調用對象可以是函數、方法、類或是實現了call方法的類實例。

以下借助簡單的實例來了解最主要的兩種實現:函數和類

# 函數實現

# 可調用對象    接收兩個參數
def hello(environ, start_response):
    # 響應信息
    status = '200 OK'
    response_headers = [('Content-type', 'text/html')]
    # 需要在可調用函數中調用的函數
    start_response(status, response_headers)
    # 返回可迭代對象
    return [b'<h1>Hello</h1>']

注:WSGI規定請求和響應主體應該為字符串(bytestrings),即py2中的str。在py3中字符串默認為unicode類型,因此需要在字符串前添加b聲明為bytes類型,兼容兩者

# 類實現

class AppClass:
    
    def __init__(self, environ, start_response):
        self.environ = environ
        self.statr = start_response
    
    # iter方法,這個類被迭代時,調用這個方法
    # 實現該方法的類就是迭代器
    def __iter__(self):
        status = '200 OK'
        response_headers = [('Content-type', 'text/html')]
        self.start(status, response_headers)
        yield b'<h1>Hello</h1>'

werkzeug中如何實現Web程序

由于flask是基于werkzeug實現的,所以先了解以下werkzeug是如何實現一個簡單的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規定了ip、端口號、調用對象

路由是怎么設定的?

Werkzeug怎么實現路由系統

# 路由表
m = Map()
rule1 = Rule('/', endpoint='index')
rule2 = Rule('/downloads/', endpoint='downloads/index')
m.add(rule1)
m.add(rule2)

Flask的路由系統

Flask使用中的路由系統,是通過route() 裝飾器來將視圖函數注冊為路由。進入route函數

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,并將函數作為參數傳入。看到add_url_rule存在關鍵的語句

# url_map實際上就是Map類的實例
# rule就是通過route相關更正成的Rule實例
self.url_map.add(rule)

# view_functions是一個字典,存儲了端點和視圖函數的映射關系。可用于查詢
self.view_functions[endpoint] = view_func

再進入底層就會發現,實際上就同上例的werkzeug實現

導入config配置參數

最初,我們修改配置文件會使用以下方法

app.config['DEGUB'] = True

導入參數

import config
app.config.from_object(config)

# 在config.py 文件中 存放配置參數
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() 函數 [位于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函數中

module_name, obj_name = import_name.rsplit(".", 1)
module = __import__(module_name, globals(), locals(), [obj_name])

dir()函數的作用:

dir() 函數不帶參數時,返回當前范圍內的變量、方法和定義的類型列表;
帶參數時,返回參數的屬性、方法列表。
如果參數包含方法__dir__(),該方法將被調用。
如果參數不包含__dir__(),該方法將最大限度地收集參數信息。

獲取屬性后判斷是否為大寫,是則添加為配置參數

用類導入配置的作用

在開發和線上,往往采用的不是相同的配置文件。我們可以通過類封裝幾套配置文件以供使用。

可以編寫一個基礎類,在開發測試、線上運行都相同、都需要的配置參數。再通過繼承,擴展不同環境下的不同配置參數。

則在不同的環境下,只需要改變from_object() 中的參數即可。

Flask如何處理請求

app程序對象

在一些Python web框架中,視圖函數類似

@route('/')
def index():
    return 'hello'

但在flask中

@app.route('/')
def index():
    return 'hello'

flask 中存在一個顯式的程序對象,我們需要在全局空間中創建它。設計原因主要包括:

  • 相較于隱式程序對象,同一時間只能有一個實例存在,顯式的程序對象允許多個程序實例存在。
  • 允許通過子類化Flask類來改變程序行為。
  • 允許通過工廠函數來創建程序實例,可以在不同的地方傳入不同的配置來創建不同的程序實例。
  • 允許通過藍本來模塊化程序。

啟動app.run()

在Flask類中

當調用app.run(),程序啟動。我們查看run()函數的源碼

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函數,而第三個參數是self,即flask對象。

當調用對象時,python會執行__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時,由于實現了__call__函數,則通過該函數調用了wsgi_app()函數

具體分析wsgi_app函數:

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

詳情查看:

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

before_request\after_request

在平常使用中,我們還會使用裝飾器before_request對某些請求執行前做一些相關操作。

我們進入before_request源碼中,可以看到實際上就一行代碼

def before_request(self, f):
    self.before_request_funcs.setdefault(None, []).append(f)
    return f

并且從源碼中可以看到before_request_funcs只是Flask類中初始化的一個空字典。所以以上函數就是將字典設置為

{
    None : [func1, func2...]    
}

鍵為none,值為存儲了before_request函數的列表

回頭再看到當請求到達時,__call__調用wsgi_aqq函數

# 先是將請求相關的資源環境封裝成請求上下文對象 并入棧
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)中的的函數遍歷出來執行。如果存在返回值,則直接返回。

所有如果當前的before_request函數存在并且返回了值,則之后的函數before_request函數后不會被執行,并且視圖函數也不會執行,可見調用before_request的源碼(前文已提到)

rv = self.preprocess_request()
# 若不存在返回值, 才執行視圖函數 
if rv is None:
    rv = self.dispatch_request()
# 否則處理錯誤
except Exception as e:
    rv = self.handle_user_exception(e)

# 執行后處理 生成最終的response
return self.finalize_request(rv)

再看一下finalize_request

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

所以總結流程就是:

  1. preprocess_request函數執行預處理(例before_request)
  2. 若相關預處理函數出現返回值,提前結束
  3. 若正常執行完所有預處理函數,無返回值
  4. 調用dispatch_request,執行視圖函數,將結果封裝成rv
  5. 將視圖函數生成的返回值rv傳遞給finalize_request,生成響應對象并且執行后處理

整理flask請求進入的邏輯

wsgi ( run_simple函數等待請求到來)
        ↓
調用flask的 __call__ ( 由于run_simple的self參數)
        ↓
__call__ 返回調用 wsgi_app()    
        →           ctx = self.request_context(environ) 把請求相關信息傳入初始化得一個ctx對象(請求上下文)    
            ctx.push() 將上下文對象入棧(localStack) → Local存儲(維護__storage__ = {122:{stack:[ctx,]}})
    
        ↓
視圖函數從localStack(再從local)中取出上下文進行操作

[圖片上傳失敗...(image-171246-1565862389864)]

關于Local

通過上述關系,可知local是作為一個動態的存儲倉庫。通過線程/進程id設置其運行環境(上下文)。

進入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函數中 調用了object類的setattr。但實際上本類中也存在,甚至可以不使用setattr,直接用賦值語句 __storage__ = {}也可。那為什么要調用父類的setattr呢。

回到Local的作用:動態的存儲運行環境。
Local采用__storage__作為倉庫存儲
那么面臨兩個問題:
1. 初始化__storage__
2. 動態賦值(格式為__storage__ :{122:{stack:[ctx,]}})

解決動態賦值問題,即重寫賦值函數(賦值語句的實質就是調用__setattr__)
從源碼中可以看到Local類重寫了__setattr__函數,實現了所需的要求

那么此時該如何初始化__storage__呢
由于我們新重寫的setattr函數中調用了storage,但未初始化之前就使用了它,明顯錯誤

于是使用object的setattr函數來初始化storage,就完美的解決了以上問題。

關于LocalStack

注:在local中 __storage__的實質是字典,它的val也是字典(不同進程線程的存儲空間),val的key名為stack(源碼規定), val的val是列表(用棧實現)(用于管理上下文)

在單次請求中,我們真正要使用的是當前環境下的上下文,所以如果只依靠Local:

obj = Local()
obj.stack = []
obj.stack.append(上下文環境)

顯然不易于維護、可擴展性差

于是使用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中創建了一個Local對象,此時storage是一個空字典
  2. 當調用push時,即傳入線程或進程對象時,先判斷是否已存在,否則新創建一個空間(列表,作為棧),入棧
  3. 當調用top時,返回棧頂元素
  4. 調用pop時若棧中只剩一個元素,則取出后刪除該棧空間,否則pop棧頂元素

在上下文之前

在解釋上下文之前,先看看上下文和以上的棧有什么聯系

通過以上實現的棧,我們做出以下假設,用上下文存儲當前請求的環境(包括request信息、session等)

# 請求上下文
class RequestContext(object):
    def __init__(self):
        self.request = "xx"
        self.session = "oo"

# 初始化一個存儲棧空間
xxx = LocalStack()

# 當請求進入時,初始化一個請求上下文、封裝了當前環境
ctx = RequestContext()

# 將該請求上下文入棧
xxx.push(ctx)

# 當需要使用相關資源時,取當前棧頂元素,即可操作相關數據
obj = xxx.top()
obj.request
obj.session

具體源碼下章解析

本地上下文

以上所談及的上下文究竟是什么呢?

在多線程環境下,要想讓所有視圖函數都獲取請求對象。

  • 最直接的方法就是在調用視圖函數時將所有需要的數據作為參數傳遞進去,但這樣一來程序邏輯就變得冗余不易于維護。
  • 另一種方法是將這些數據設為全局變量,但是這樣必然會在不同的線程中出現混亂(非線程安全)。
    本地線程(thread locals) 的出現解決了這些問題。

本地線程就是一個全局對象,使用一種特定線程且線程安全的方式來存儲和獲取數據。也就是說,同一個變量在不同線程內擁有各自的值,互不干擾。

實現原理其實很簡單,就是根據線程的ID來存取數據。

Flask沒有使用標準庫的threading.local(),而是使用了Werkzeug自己實現的本地線程對象werkzeug.local.Local(),后者增加了對Greenlet(以C擴展形式接入python的輕量級協程)的優先支持。

Flask使用本地線程來讓上下文代理對象全局可訪問,比如:

  • request
  • session
  • current_app
  • g

這些對象被稱為本地上下文對象(context locals)。

所以,在不基于線程、greenlet或單進程實現的并發服務器上,這些代理對象將無法正常工作,但僅有少部分服務器不支持。

Flask的設計初衷是為了讓傳統Web程序開發更加簡單和迅速,二不是用來開發大型程序或異步服務器的。但Flask 的可擴展性卻提供了無限的可能性,除了使用擴展,還可以子類化Flask類或為程序添加中間件。

應用上下文、請求上下文都是對象,是對一系列flask對象的封裝,并且提供相關的接口方法。

  • 請求上下文: request session
  • 應用上下文: app g
  • flask中上下文相關的代碼存放在 ctx.py

請求上下文

請求上下文最主要的是提供對Request請求對象的封裝。

RequestContext(object)  // 請求上下文
    - __init__
    - push
    - pop
    - __enter__
    - __exit__

先看源碼中init函數的作用

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

可以看到就是對當前請求相關數據的初始化,如 當前app對象、request、session、flashes等,符合上章所提到的上下文和棧的關系作用。

認識

請求到來時:

# self是app對象,environ是請求相關的原始數據(根據WSGI規定)
ctx = RequestContext(self, environ)
ctx.request = Request(environ)
ctx.session = None

# 不同的線程在內部分別持有不同的資源
{
    1232:{ctx: ctx對象}
    1231:{ctx: ctx對象}
    2141:{ctx: ctx對象}
    1235:{ctx: ctx對象}
}

視圖函數:

from flask import request,session
# falsk 自動的識別當前線程,找到對應的ctx里的request、session

請求結束:

根據當前線程的唯一標記,將數據資源移除

實現

flask利用local()為線程或協程開辟資源空間,并用stack【棧】存儲維護,內部再使用偏函數【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相當于將單個線程或協程的數據資源分割開來,并作為棧進行維護
6. Local __storage__ = {
    1231: {stack: [ctx(request, session), ]}
                        }
    - local的結構。存儲了多個線程或協程的資源數據
7. session存儲
    根據請求中的cookie提取名為sessionid對應的值,對cookie加密+反序列化,再賦值給ctx里的session
8. 視圖函數
    - 利用flask已經封裝好的庫,調用session或request的相關資源
9. 操作結束后
    把session中的數據再次寫入cookie中,將ctx刪除
10. 結果返回給用戶瀏覽器
11. 斷開socket連接

request哪來的

  1. 首先當請求進入時,__call__調用wsgi_app
  2. 在wsgi_app中初始化了一個請求上下文 ctx = self.request_context(environ)
  3. 可見是將environ作為參數傳入,而在WSGI中規定 environ即保存著請求相關的數據
  4. 進入request_context() 函數 發現只有一行代碼 return RequestContext(self, environ)
  5. 進入RequestContext類 看到init函數中 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數據保存到redis
  2. 生成一個隨機字符串
  3. 返回一個隨機字符串給用戶,并作為key
  4. 客戶端再訪問時返回該隨機字符串

flash

flask中存在消息閃現機制,通過flash()源碼(helpers.py)可以看到,本質上是利用session實現的

# 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信息函數

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

則最終實現的效果是 flash() 存入信息,get_flashed_messages()只能對應的彈出一次。

應用上下文

應用上下文最主要的就是提供對核心對象flask的封裝。

源代碼中類的主要結構為:

AppContext(object)      // 應用上下文
    - push
    - pop
    - __enter__
    - __exit__

g

每個請求進入時,都會創建一個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

主要通過棧實現,即當一個請求進入時:

  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 用于創建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)

由于以上判斷,所以我們在視圖函數中使用current_app時,由于有請求上下文,所以不需要手動將應用上下文app_ctx入棧。如果在視圖函數外,沒有請求發生時,使用current_app則需要手動入棧

app_ctx = app.app_context()
app_ctx.push()
# 可使用current_app
app_ctx.pop()

何時會用到?

在實際生產中,current_app對象一般都是至與視圖函數中使用
由于有正在的請求到來,所以不需要手動入棧。
但是在代碼測試階段,在進行單元測試時,或離線應用(不使用postman等工具發生完整請求)
沒有實際的請求到來,又需要對代碼進行測試
則需要手動將app_ctx入棧

with優化出入棧

# with優化 不需要手動push pop
with app.app_context():     
    # __enter__(連接) 
    a = current_app     
    d = current_app.config['DEBUG']
    #  __exit__(釋放連接【資源】)
    # (__exit__內部實現了異常處理,若成功處理了返回True,若沒有成功處理,返回False還會向外部拋出異常)
# 出了with環境 app對象被pop()出棧 current_app 就找不到目標了
 
# with可以對實現了上下文協議的對象使用
# 上下文管理器(app context)
# 實現了__enter__(連接)  __exit__(釋放連接【資源】)就是上下文管理器
# 上下文表達式必須要返回一個上下文管理器

# 此時a是__enter__ 的返回值
with app.app_context() as a:    
    pass


# 可以自己實現上下文管理器,必須實現__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做返回,使用結束后再回到函數關閉連接
    yield MyResource()
    print('close connection')

with MyResource() as r:
        r.query()

# 但是更好的做法是將本身不是上下文管理器的類,變為上下文管理器
# 例:輸入書名 with中自動添加 《》 
#     操作數據庫 with中自動連接、回滾、斷開

源碼中的體現

從源碼中可以看到無論是應用上下文還是請求上下文,都具有以下兩個函數

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代理

代理有什么用?

所有的數據都存儲在Local中,如果直接對數據進行存取,需要建立多個類進行對數據的存取。如request類、session類、g類、current_app類。

但是由于以上類的功能相同,可以抽象出來,使用一個代理類,完成所需功能。

知識預備

# 偏函數
import functools

def index(a1, a2)
    return a1 + a2

new_func = functools.partial(index, 666)
# 幫助自動傳遞參數
new_func(1)     // 667

源碼體現

在我們實際運用中,并不是直接去操作上下文。而是使用例如: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函數

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)

即為該對象設置值,而我們在實例化的時候,傳遞的參數是一個偏函數

那么當我們創建完代理對象后,考慮我們是怎樣使用這些代理的: 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函數

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()實際上就是我們傳遞進來的偏函數(init()初始化的結果)

回頭看一下傳遞進來的偏函數,看到源碼中的_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)

即取出棧頂的元素(上下文),再通過getattr獲取到相關的內容。而erquest、session等,在前面也已經看到,是在上下文初始化時就創建的。所以該函數最終就是根據傳遞進來的參數(request, session, g, current_app),進入到local棧中,top拿到棧頂的上下文,然后在上下文中取出所需的資源。

三種程序狀態

Flask提供的四個本地上下文對象分別在特定的程序狀態下綁定實際的對象。如果我們在訪問或使用它們時還沒有綁定,就會看到經典的RuntimeError異常。

Flask中存在三種狀態:

  • 程序設置狀態
  • 程序運行狀態
  • 請求運行狀態

程序設置狀態

當Flask類被實例化,也就是創建程序實例app后,就進入程序設置狀態。這是所有的全局對象都沒有被綁定:

app = Flask(__name__)

程序運行狀態

當Flask程序啟動,但是還沒有請求進入時,Flask進入了程序運行狀態。
在這種狀態下,程序上下文對象current_app和g都綁定了各自的對象。

使用flask shell命令打開的python shell默認就是這種狀態,我們也在普通的Python shell中通過手動推送程序上下文來模擬:

app = Flask(__name__)
ctx = app.app_context()
ctx.push()
# current_app g     /Flask flask.g
# requst session  /unbound

以上我們手動使用app_context() 創建了程序上下文,然后調用push() 方法把它推送到程序上下文堆棧里。

默認情況下,當請求進入的時候,程序上下文會隨著請求上下文一起被自動激活。但是在沒有請求進入的場景,比較離線腳本、測試或者進行交互調試的時候,手動推送程序上下文以進入程序運行狀態會非常方便。

請求運行狀態

當請求進入的時候,或是使用test_request_context()方法、test_client()方法時,Flask會進入請求運行狀態。因為當請求上下文被推送時,程序上下文會被自動推送,所以在這個狀態下4個全局對象都會被綁定。我們可以通過手動推送請求上下文模擬:

app = Flask(__name__)
ctx = app.test_request_context()
ctx.push()
# current_app, g, request, session
# Flask flask.g Request NullSession

這也是為什么可以直接在視圖函數和相應的回調函數里直接使用這些上下文對象,而不用推送上下文(Flask在處理請求時會自動推送請求上下文和程序上下文)

引用

  • 《Flask Web 開發實戰》
  • 各類視頻資料...
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內容