Flask核心機制--上下文源碼剖析

来源:https://www.cnblogs.com/wdliu/archive/2018/12/19/10144933.html
-Advertisement-
Play Games

一、前言 瞭解過flask的python開發者想必都知道flask中核心機制莫過於上下文管理,當然學習flask如果不瞭解其中的處理流程,可能在很多問題上不能得到解決,當然我在寫本篇文章之前也看到了很多博文有關於對flask上下文管理的剖析都非常到位,當然為了學習flask我也把對flask上下文理 ...


一、前言

  瞭解過flask的python開發者想必都知道flask中核心機制莫過於上下文管理,當然學習flask如果不瞭解其中的處理流程,可能在很多問題上不能得到解決,當然我在寫本篇文章之前也看到了很多博文有關於對flask上下文管理的剖析都非常到位,當然為了學習flask我也把對flask上下文理解寫下來供自己參考,也希望對其他人有所幫助。

二、知識儲備

threadlocal

  在多線程中,線程間的數據是共用的, 但是每個線程想要有自己的數據該怎麼實現? python中的threading.local對象已經實現,其原理是利用線程的唯一標識作為key,數據作為value來保存其自己的數據,以下是demo演示了多個線程同時修改同一變數的值的結果:

#!/usr/bin/env python3
# -*- coding:utf-8 -*-
# Author:wd

import threading
import time
values=threading.local()

def run(arg):
    values.num=arg  #修改threading.local對象的name數據
    time.sleep(1)
    print(threading.current_thread().name,values.num)  #列印values.num


for i in range(3):
    th = threading.Thread(target=run, args=(i,), name='run thread%s' % i)
    th.start()


結果:
run thread0 0
run thread1 1
run thread2 2

結果說明:

從結果中可以看到,values.num的值是不同的,按照普通線程理解因為有sleep存在,在每個線程最後列印values.num時候值應該都是2,但是正是因為threading.local對象內部會為每個線程開闢一個記憶體空間,從而使得每個線程都有自己的單獨數據,所以每個線程修改的是自己的數據(內部實現為字典),列印結果才不一樣。

有了以上的設計思想,我們可以自己定義類似於thread.local類,為了支持協程,將其唯一標識改為協程的唯一標識,其實這已經及其接近flask中的Local類了(後續在進行說明):

try:
    from greenlet import getcurrent as get_ident  # 攜程唯一標識
except ImportError:
    try:
        from thread import get_ident
    except ImportError:
        from _thread import get_ident  # 線程唯一標識


class Local(object):
    def __init__(self):
        object.__setattr__(self, 'storage', dict())  # 防止self.xxx 遞歸
        object.__setattr__(self, '__get_ident__', get_ident)

    def __setattr__(self, key, value):
        ident = self.__get_ident__()  # 獲取當前線程或協程的唯一標識
        data = self.storage.get(ident)
        if not data:  # 當前線程沒有數據
            data = {key: value}  # 創建數據
        else:  # 當前已經有數據
            data[key] = value

        self.storage[ident] = data  # 最後為當前線程設置其標識對應的數據

    def __getattr__(self, name):
        try:
            return self.storage[self.__get_ident__()].get(name)  # 返回name所對應的值
        except KeyError:
            raise AttributeError(name)

functools.partial

  partial函數是工具包的一個不常用函數,其作用是給函數傳遞參數,同時返回的也是這個函數,但是這個函數的已經帶了參數了,示例:

from functools import partial

def func(x,y,z):
    print(x,y,z)

new_fun=partial(func,1,2)  #生成新的函數,該函數中已經有一個參數
new_fun(3)

結果:
1 2 3

在以上示例中,new_func是由func生成的,它已經參數1,2了,只需要傳遞3即可運行。

werkzeug

  werkzeug是一個實現了wsgi協議的模塊,用官方語言介紹:Werkzeug is a WSGI utility library for Python. It's widely used and BSD licensed。為什麼會提到它呢,這是因為flask內部使用的wsgi模塊就是werkzeug,以下是一個示例(如果你瞭解wsgi協議的應該不用過多介紹):

from werkzeug.wrappers import Request, Response

@Request.application
def application(request):
    return Response('Hello World!')

if __name__ == '__main__':
    from werkzeug.serving import run_simple
    run_simple('localhost', 4000, application)

在示例中application是一個可調用的對象也可以是帶有__call__方法的對象,在run_simple內部執行application(),也就是在源碼的execute(self.server.app)中執行,這裡你只需要run_simple會執行第三個參數加括弧。

三、源碼剖析

上下文管理

   在說請求上下文之前先看一個flask的hell world示例:

from flask import Flask

app=Flask(__name__)
@app.route("/")
def hello():
    return 'hello world'

if __name__=='__main__':
    app.run()

在以上示例中,app.run是請求的入口,而app是Flask實例化的對象,所以執行的是Flask類中的run方法,而在該改方法中又執行了run_simple方法,以下是run方法部分源碼摘抄(其中self就是app對象):
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中會執行app(environ, start_response),參考werkzeug的源碼,源碼會執行app(environ, start_response)也就是執行app的__call__方法,以下是__call__方法源碼摘抄:
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)

__call__方法中又調用了wsgi_app方法,該方法也就是flask的核心所在,下麵是方法摘抄:

def wsgi_app(self, environ, start_response):
    """The actual WSGI application. This is not implemented in
    :meth:`__call__` so that middlewares can be applied without
    losing a reference to the app object. Instead of doing this::

        app = MyMiddleware(app)

    It's a better idea to do this instead::

        app.wsgi_app = MyMiddleware(app.wsgi_app)

    Then you still have the original application object around and
    can continue to call methods on it.

    .. versionchanged:: 0.7
        Teardown events for the request and app contexts are called
        even if an unhandled error occurs. Other events may not be
        called depending on when an error occurs during dispatch.
        See :ref:`callbacks-and-errors`.

    :param environ: A WSGI environment.
    :param start_response: A callable accepting a status code,
        a list of headers, and an optional exception context to
        start the response.
    """
    #ctx.app 當前app名稱
    #ctx.request request對象,由app.request_class(environ)生成
    #ctx.session session 相關信息
    ctx = self.request_context(environ) 
    error = None
    try:
        try:
            ctx.push()
            #push數據到local,此時push的數據分請求上線文和應用上下文
            # 將ctx通過Localstack添加到local中
            # app_ctx是APPContext對象
            response = self.full_dispatch_request()
        except Exception as e:
            error = e
            response = self.handle_exception(e)
        except:
            error = sys.exc_info()[1]
            raise
        return response(environ, start_response)
    finally:
        if self.should_ignore_error(error):
            error = None
        ctx.auto_pop(error)

第一句:ctx = self.request_context(environ)調用request_context實例化RequestContext對象,以下是RequestContext類的構造方法:

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

此時的request為None,所以self.request=app.request_class(environ),而在Flask類中request_class = Request,此時執行的是Request(environ),也就是實例化Request類,用於封裝請求數據,最後返回RequestContext對象,此時的ctx含有以下屬性ctx.app(app對象)、ctx.request(請求封裝的所有請求信息)、ctx.app(當前app對象)等

第二句:ctx.push(), 調用RequestContext的push方法,以下是源碼摘抄:
def push(self):
    """Binds the request context to the current context."""
    # If an exception occurs in debug mode or if context preservation is
    # activated under exception situations exactly one context stays
    # on the stack.  The rationale is that you want to access that
    # information under debug situations.  However if someone forgets to
    # pop that context again we want to make sure that on the next push
    # it's invalidated, otherwise we run at risk that something leaks
    # memory.  This is usually only a problem in test suite since this
    # functionality is not active in production environments.
    top = _request_ctx_stack.top
    if top is not None and top.preserved:
        top.pop(top._preserved_exc)

    # Before we push the request context we have to ensure that there
    # is an application context.
    app_ctx = _app_ctx_stack.top  #獲取應用上線文,一開始為none
    if app_ctx is None or app_ctx.app != self.app:
        # 創建APPContext(self)對象,app_ctx=APPContext(self)
        # 包含app_ctx.app  ,當前app對象
        # 包含app_ctx.g  , g可以看作是一個字典用來保存一個請求周期需要保存的值
        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()
    #self 是RequestContext對象,其中包含了請求相關的所有數據
    _request_ctx_stack.push(self)

    # Open the session at the moment that the request context is available.
    # This allows a custom open_session method to use the request context.
    # Only open a new session if this is the first time the request was
    # pushed, otherwise stream_with_context loses the session.
    if self.session is None:
        session_interface = self.app.session_interface  # 獲取session信息
        self.session = session_interface.open_session(
            self.app, self.request
        )

        if self.session is None:
            self.session = session_interface.make_null_session(self.app)

到了這裡可以看到,相關註解已經標註,flask內部將上下文分為了app_ctx(應用上下文)和_request_ctx(請求上下文),並分別用來兩個LocalStack()來存放各自的數據(以下會用request_ctx說明,當然app_ctx也一樣),其中app_ctx包含app、url_adapter一下是app_ctx構造方法:
def __init__(self, app):
    self.app = app
    self.url_adapter = app.create_url_adapter(None)
    self.g = app.app_ctx_globals_class()

    # Like request context, app contexts can be pushed multiple times
    # but there a basic "refcount" is enough to track them.
    self._refcnt = 0

然後分別執行app_ctx.push()方法和_request_ctx_stack.push(self)方法,將數據push到stack上,_request_ctx_stack.push(self),而_request_ctx_stack是一個LocalStack對象,是一個全局對象,具體路徑在flask.globals,以下是其push方法: 
def push(self, obj):
    """Pushes a new item to the stack"""
    #找_local對象中是否有stack,沒有設置rv和_local.stack都為[]
    rv = getattr(self._local, 'stack', None)
    if rv is None:
        self._local.stack = rv = []
        # 執行Local對象的__setattr__方法,等價於a=[],rv=a, self._local.stack =a
        #創建字典,類似於storage={'唯一標識':{'stack':[]}}
    rv.append(obj)
        #列表中追加請求相關所有數據也就是storage={'唯一標識':{'stack':[RequestContext對象,]}}
    return rv

以上代碼中的self._local是一個Local()對象源碼定義如下,也就是用於存儲每次請求的數據,和我們剛開始定義的local及其相似,這也是為什麼要先提及下threadlocal。
class Local(object):
    __slots__ = ('__storage__', '__ident_func__')

    def __init__(self):
        object.__setattr__(self, '__storage__', {})
        object.__setattr__(self, '__ident_func__', get_ident)

    def __iter__(self):
        return iter(self.__storage__.items())

    def __call__(self, proxy):
        """Create a proxy for a name."""
        return LocalProxy(self, proxy)

    def __release_local__(self):
        self.__storage__.pop(self.__ident_func__(), None)

    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}

    def __delattr__(self, name):
        try:
            del self.__storage__[self.__ident_func__()][name]
        except KeyError:
            raise AttributeError(name)
Local()

到這裡我們知道了,當執行ctx.push()時,local對象中已經有數據了,接著開始執行self.full_dispatch_request(),也就是開始執行視圖函數,以下是源碼摘抄:
def full_dispatch_request(self):
    """Dispatches the request and on top of that performs request
    pre and postprocessing as well as HTTP exception catching and
    error handling.

    .. versionadded:: 0.7
    """
    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.preprocess_request(),用於執行所有被before_request裝飾器裝飾的函數,從源碼總可以看到如果該函數有返回,則不會執行self.dispatch_request()也就是視圖函數, 執行完畢之後調用self.dispatch_request()根據路由匹配執行視圖函數,然後響應最後調用ctx.auto_pop(error)將stack中的數據刪除,此時完成一次請求。  

全局對象request、g、session

   在瞭解完flask的上下文管理時候,我們在視圖函數中使用的request實際上是一個全局變數對象,當然還有g、session這裡以request為例子,它是一個LocalProxy對象,以下是源碼片段:

request = LocalProxy(partial(_lookup_req_object, 'request'))

當我們使用request.path時候實際上是調用是其__getattr__方法即LocalProxy對象的__getattr__方法,我們先來看看LocalProxy對象實例化的參數:

def __init__(self, local, name=None):
    #local是傳入的函數,該句等價於self.__local=local,_類名__欄位強行設置私有欄位值
    #如果是requst則函數就是partial(_lookup_req_object, 'request')
    object.__setattr__(self, '_LocalProxy__local', local)
    object.__setattr__(self, '__name__', name) #開始的時候設置__name__的值為None
    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)

在源碼中實例化時候傳遞的是partial(_lookup_req_object, 'request')函數作為參數,也就是self.__local=該函數,partial參數也就是我們之前提到的partial函數,作用是傳遞參數,此時為_lookup_req_object函數傳遞request參數,這個在看看其__getattr__方法:

def __getattr__(self, name):
    #以獲取request.method 為例子,此時name=method
    if name == '__members__':
        return dir(self._get_current_object())
    #self._get_current_object()返回的是ctx.request,再從ctx.request獲取method (ctx.request.method)
    return getattr(self._get_current_object(), name)

在以上方法中會調用self._get_current_object()方法,而_get_current_object()方法中會調用self.__local()也就是帶參數request參數的 _lookup_req_object方法從而返回ctx.request(請求上下文),最後通過然後反射獲取name屬性的值,這裡我們name屬性是path,如果是request.method name屬性就是method,最後我們在看看_lookup_req_object怎麼獲取到的ctx.request,以下是源碼摘抄:

def _lookup_req_object(name):
    #以name=request為列
    top = _request_ctx_stack.top
    # top是就是RequestContext(ctx)對象,裡面含有request、session 等
    if top is None:
        raise RuntimeError(_request_ctx_err_msg)
    return getattr(top, name) #到RequestContext(ctx)中獲取那麼為request的值

在源碼中很簡單無非就是利用_request_ctx_stack(也就是LocalStack對象)的top屬性返回stack中的ctx,在通過反射獲取request,最後返回ctx.request。以上是整個flask的上下文核心機制,與其相似的全局對象有如下(session、g):
# context locals
_request_ctx_stack = LocalStack()  #LocalStack()包含pop、push方法以及Local對象,上下文通過該對象push和pop
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, 'request’))  #reuqest是LocalProxy的對象,設置和獲取request對象中的屬性通過LocalProxy定義的各種雙下劃線實現
session = LocalProxy(partial(_lookup_req_object, 'session'))
g = LocalProxy(partial(_lookup_app_object, 'g'))  

技巧應用

利用flask的上下文處理機制我們獲取上請求信息還可以使用如下方式:

from flask import Flask,_request_ctx_stack

app=Flask(__name__)

@app.route("/")
def hello():
    print(_request_ctx_stack.top.request.method) #結果GET,等價於request.method
    return ’this is wd'

if __name__=='__main__':
    app.run()

 

 

 

  


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Python學習心得——模塊的導入 在Python語言的編輯器里,除了預設的內置函數外,其他函數的調用,必須先通過import語句將其導入才能使用。 import語句導入整個函數模塊 導入方法: import 函數模塊名 示例 新建一個名為 func_test.py 的文件,內容為: 保存並退出,在 ...
  • 前言 本篇主要講述是Java中JDK1.8的一些新語法特性使用,主要是Lambda、Stream和LocalDate日期的一些使用講解。 Lambda Lambda介紹 Lambda 表達式(lambda expression)是一個匿名函數,Lambda表達式基於數學中的λ演算得名,直接對應於其中 ...
  • 1.變數名 命名規則: 在名稱中只能使用字母字元、數字和下劃線; 名稱的第一個字元不能是數字; 不能將C++關鍵詞用作名稱。 常用的首碼:n-整數,str/sz-字元串,b-布爾值,p-指針,c-單個字元,m-類成員值 2.整形 short 至少16位; int至少與short一樣長; long至少 ...
  • 1.文件讀模式 r f = open("helloworld", 'r', encoding="utf-8") 文件句柄: "helloworld" 表示讀的文件的文件名, 'r' 代表讀模式, encoding="utf-8" 表示字元編碼形式為utf-8。 有open就有close,不管是讀文件 ...
  • Python基礎知識(37):訪問資料庫(Ⅱ) 因臨近考試,本人即將進入複習階段,從明天12月19號至2019年的1月二十多號暫停更新 二、MySQL MySQL是Web世界中使用最廣泛的資料庫伺服器。SQLite的特點是輕量級、可嵌入,但不能承受高併發訪問,適合桌面和移動應用。而MySQL是為服務 ...
  • 在我們的世界中事物和事物之間總會有一些聯繫. 在面向對象中. 類和類之間也可以產生相關的關係 1. 依賴關係 執行某個動作的時候. 需要xxx來幫助你完成這個操作. 此時的關係是最輕的. 隨時可以更換另外一個東西來完成此操作 2. 關聯關係 在對象裡面埋對象 1. 一對一關係 2. 一對多關係 類中 ...
  • 本文說明瞭如何定製化JHipster-Registry,增加消息匯流排功能。 ...
  • 前言: 最近有人在Twisted郵件列表中提出諸如”為任務緊急的人提供一份Twisted介紹”的的需求。值得提前透露的是,這個序列並不會如他們所願.尤其是介紹Twisted框架和基於Python 的非同步編程而言,可能短時間無法講清楚。因此,如果你時間緊急,這恐怕不是你想找的資料。 我相信如果對非同步編 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...