Flask中的上下文

flask中的上下文分兩種,application context和request context,即應用上下文和請求上下文。python

從名字上看,可能會有誤解,認爲應用上下文是一個應用的全局變量,全部請求均可以訪問修改其中的內容;而請求上下文則是請求內可訪問的內容。
但事實上,這二者並非全局與局部的關係,它們都處於一個請求的局部中。web

先說結論:每一個請求的g都是獨立的,而且在整個請求內都是可訪問修改的。面試

下面來研究一下。redis

上下文類的定義:sql

上下文類定義在flask.ctx模塊中shell

class AppContext(object):
    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 = 0django

查看了源代碼,AppContext類便是應用上下文,能夠看到裏面只保存了幾個變量,其中比較重要的有:
app是當前web應用對象的引用,如Flask;還有g,用來保存須要在每一個請求中須要用到的請求內全局變量。編程


class RequestContext(object):
    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 = Noneflask

        # Request contexts can be pushed multiple times and interleaved with
        # other request contexts.  Now only if the last level is popped we
        # get rid of them.  Additionally if an application context is missing
        # one is created implicitly so for each level we add this information
        self._implicit_app_ctx_stack = []

RequestContext即請求上下文,其中有咱們熟悉的request和session,app和應用上下文中的app含義相同。

上下文對象的做用域

那麼這兩種上下文運行時是怎麼被使用的呢?

線程有個叫作ThreadLocal的類,也就是一般實現線程隔離的類。而werkzeug本身實現了它的線程隔離類:werkzeug.local.Local。LocalStack就是用Local實現的。

在flask.globals模塊中定義了兩個LocalStack對象:
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()

LocalStack是flask定義的線程隔離的棧存儲對象,分別用來保存應用和請求上下文。
它是線程隔離的意思就是說,對於不一樣的線程,它們訪問這兩個對象看到的結果是不同的、徹底隔離的。這是根據pid的不一樣實現的,相似於門牌號。

而每一個傳給flask對象的請求,都是在不一樣的線程中處理,並且同一時刻每一個線程只處理一個請求。因此對於每一個請求來講,它們徹底不用擔憂本身上下文中的數據被別的請求所修改。

而後就能夠解釋這個特性:從flask模塊中引入的g、session、request、current_app是怎麼作到同一個對象能在全部請求中使用而且不會衝突。

這幾個對象仍是定義在flask.globals中:
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類的構造函數接收一個callable參數,上面這幾個就傳入了一個偏函數。以g爲例,當對g進行操做時,就會調用做爲參數的偏函數,並把操做轉發到偏函數返回的對象上。

查看這幾個函數的實現:
def _lookup_req_object(name):
    top = _request_ctx_stack.top
    if top is None:
        raise RuntimeError('working outside of request context')
    return getattr(top, name)


def _lookup_app_object(name):
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError('working outside of application context')
    return getattr(top, name)


def _find_app():
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError('working outside of application context')
    return top.app

因爲_app_ctx_stack和_request_ctx_stack都是線程隔離的,因此對g的調用就是這樣一個過程:

訪問g-->從當前線程的應用上下文棧頂獲取應用上下文-->取出其中的g對象-->進行操做。
因此能夠經過一個g對象而讓全部線程互不干擾的訪問本身的g。

上下文對象的推送

構建Flask對象後並不會推送上下文,而在Flask對象調用run()做爲WSGI 應用啓動後,每當有請求進入時,在推送請求上下文前,若是有必要就會推送應用上下文。但運行了run就會阻塞程序,因此在shell中調試時,必須手動推送上下文;或者使用flask-scripts,它運行的任務會在開始時自動推送。

上面加粗的「若是有必要」,那麼什麼叫有必要呢?是否是意味着在每一個線程裏應用上下文只會被推送一次、一次請求結束下一次請求來的時候就不用再推送應用上下文了呢?
來看RequestContext的源碼,push函數:
def push(self):    
    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
    if app_ctx is None or app_ctx.app != self.app:
        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)

flask在推送請求上下文的時候調用push函數,他會檢查當前線程的應用上下文棧頂是否有應用上下文;若是有,判斷與請求上下文是否屬於同一個應用。在單WSGI應用的程序中,後者的判斷無心義。
此時,只要沒有應用上下文就會推送一個當前應用的上下文,而且把該上下文記錄下來。

請求處理結束,調用auto_pop函數,其中又調用自身的pop函數:
def pop(self, exc=None): 
   app_ctx = self._implicit_app_ctx_stack.pop()
         ………………      
      ………………
   rv = _request_ctx_stack.pop()   
    # Get rid of the app as well if necessary.
    if app_ctx is not None:
        app_ctx.pop(exc)

會把請求上下文和應用上下文都pop掉。

故,在單WSGI應用環境下,每一個請求的兩個上下文都是徹底獨立的(獨立於線程上曾經的請求,獨立於其餘線程的請求)。Q.E.D

那麼,何時不必推送呢?事實上,每次請求到來的時候都會推送,都是有必要的。由於當Flask在做爲WSGI應用運行的時候,不可能出現當前線程的應用上下文已存在的狀況。

那麼就要搞清何時會有已存在的應用上下文。

研究時我參考了博文:https://blog.tonyseek.com/post/the-context-mechanism-of-flask/
該博文在最後提到了「兩個疑問」:①應用和請求上下文在運行時都是線程隔離的,爲什麼要分開來?②每一個線程同時只處理一個請求,上下文棧確定只有一個對象,爲什麼要用棧來存儲?
博主認爲,這兩個設計都是爲了在離線狀態下調試用:


因此,綜上所述,在非離線狀態下,上下文棧在每一個WSGI應用裏是獨立的,而每一個應用裏線程同時只處理一個請求,故上下文棧確定只有一個對象。而且,在請求結束後都會釋放,因此新的請求來的時候都會從新推送兩個上下文。

小結:
解釋了這麼多,對於flask編程來講,只有一個應用上的結論:每一個請求的g都是獨立的,而且在整個請求內都是可訪問修改的。

 

1、threading-local

一、threding-local

做用:爲每個線程開闢一塊空間進行數據存儲
from threading import local
from threading import Thread
import time

# 示例化local對象
ret=local()

def task(s):
    global ret
    ret.value=s
    time.sleep(2)
    print(ret.value)


# 開啓10個線程
for i in range(10):
    t=Thread(target=task,args=(i,))
    t.start()

二、自定義local

# 若是有協程則使用協程惟一標識getcurrent
try:
    from greenlet import getcurrent as get_ident
except Exception as e:
    from threading import Thread,get_ident

class Local(object):
    # 線程的惟一標識
    ident = get_ident()
    def __init__(self):
        # 執行父類__setattr__
        object.__setattr__(self,"storage",{})

    def __setattr__(self,k, v):
        """
        構造dict
        storage={
            ident:{val:0},
            ident:{val:1},
            ident:{val:3},
            ident:{val:4},
            }
        """
        if self.ident in self.storage:
            self.storage[self.ident][k] = v
        else:
            self.storage[self.ident] = {k: v}

    def __getattr__(self,k):

        return self.storage[self.ident][k]


obj=Local()

def task(arg):
    # 執行__setattr__
    obj.var=arg
    # 執行__getattr__
    v=obj.var
    print(v)

for i in range(10):
    t = Thread(target=task,args=(i,))
    t.start()

2、上下文管理源碼分析

1、上下文管理本質(相似於threading.local)
            1、每個線程都會在Local類中建立一條數據
                  {
                    「惟一標識」:{stark:[ctx,]}
                    「惟一標識」:{stark:[ctx,]}

                   }

            2、當請求進來以後,將請求相關數據添加到列表裏面[request,],之後若是使用時,就去讀取
            3、列表中的數據,請求完成以後,將request從列表中移除
2、在源碼中分析上下文管理
        
        第一階段:執行__call__--->app.wsgi-->將ctx(request,session)封裝爲RequestContent()在(open_session), app_ctx(g,app)封裝爲APPContent()經過LocalStack將這兩個類放入Local對象中
                   
        第二階段:視圖函數導入:request/session/g/app ,經過偏函數(_lookup_req_object)在經過(LocalProxy())去LocalStack中的Local類中對其進行增刪改查操做 

     第三階段:請求處理完畢        
      - 經過save_session將簽名session保存到cookie 
      -經過ctx.pop()去LocalStack中的Local類- 將ctx刪除     

有關面試問題

問題一:flask和django的區別:
  對於django來講,內部組件特別多,自身功能強大,有點大而全,而flask,內置組件不多,可是它的第三方組件不少,擴展性強,有點短小精悍,而它們之間也有類似之處,
  由於它們兩個框架都沒有寫sockte,都是基於wsgi協議作的,在此以外,flask框架中的上下文管理較爲耀眼。

  
  相同點:它們兩個框架都沒有寫sockte,都是基於wsgi協議作的
  請求相關數據傳遞的方式不一樣:django:經過傳遞request參數取值
                flask:見問題二
           組件不一樣:django組件多
                flask組件少,第三方組件豐富

問題1.1: flask上下文管理:
  簡單來講,falsk上下文管理能夠分爲三個階段:
        1、請求進來時,將請求相關的數據放入上下問管理中
        2、在視圖函數中,要去上下文管理中取值
        3、請求響應,要將上下文管理中的數據清除
  
  詳細點來講:
        1、請求剛進來,將request,session封裝在RequestContext類中,app,g封裝在AppContext類中,並經過LocalStack將requestcontext和appcontext放入Local類中
        二、視圖函數中,經過localproxy--->偏函數--->localstack--->local取值
        3、請求相應時,先執行save.session()再各自執行pop(),將local中的數據清除
        

問題1.2  flask第三方組件
  flask:
      -flask-session    默認放入cookie,能夠放入redis
      -flask-redis
      -flask-migrate
      -flask-script
      -blinker  信號
   公共: DBUtils      數據庫鏈接池
      wtforms       表單驗證+生成HTML標籤
      sqlalchemy
  自定義:Auth   參考falsk-login

問題二:Flask中的session是何時建立,何時銷燬的?
  當請求進來時,會將requset和session封裝爲一個RequestContext對象,經過LocalStack將RequestContext放入到Local對象中,由於
請求第一次來session是空值,因此執行open_session,給session(uuid4())賦值,再經過視圖函數處理,請求響應時執行save.session,將簽名session寫入cookie中,再講Local中的數值pop掉。

問題三:flask中一共有幾個LocalStack和Local對象
  兩個LocalStack,兩個Local
  request、session共同用一個LocalStack和Local
  g、app共同用一個Localstack和Local

問題四: 爲何把請求放到RequestContext中:
   由於request和session都是在視圖中操做頻繁的數據,也是用戶請求須要用的數據,將request和session封裝在RequestContext中top,pop一次就能夠完成,而單獨不封裝在一塊兒就會屢次操做,

    ctx = RequestContext(request,session)

問題五:local做用
    -保存    請求上下文對象和app上下文對象
     -localstack的源碼與threading.local(線程處理)做用類似,不一樣之處是Local是經過greenlet(協程)獲取惟一標識,粒度更細

      

 問題六:Localstack做用

    2、將local對象中的數據維護成一個棧【ctx,ctx】(先進後出)

         {
            「協程或線程的惟一標識」: { stack:[ctx,ctx,ctx,] }
}
爲何維護成一個棧?
   當是web應用時:不論是單線程仍是多線程,棧中只有一個數據
 
 

   - 服務端單線程:
    {
    111:{stack: [ctx, ]}
    }
   - 服務端多線程:
    {
    111:{stack: [ctx, ]}
    112:{stack: [ctx, ]}
    }

 
 

離線腳本:能夠在棧中放入多個數據

 
 

with app01.app_context():
  print(current_app)
  with app02.app_context():
    print(current_app)
  print(current_app)

 

問題七:什麼是g?

    g 至關於一次請求的全局變量,當請求進來時將g和current_app封裝爲一個APPContext類,在經過LocalStack將Appcontext放入Local中,取值時經過偏函數,LocalStack、loca l中取值,響應時將local中的g數據刪除:

 問題八:怎麼獲取Session/g/current_app/request

    經過 、偏函數(lookup_req_object)、Localstack、Local取值

 問題九: 技術點:
  - 反射 (LocalProxy())
  - 面向對象,封裝:RequestContext
  - 線程(threading.local)
  - 筆試:本身寫一個類+列表 實現棧。(LocalStack)

問題十:python基本哪些內容比較重要:

一、反射

  -CBV

  -django配置文件

  -wtforms中的Form()示例化中 將"_fields中的數據封裝到From類中"

二、裝飾器 (迭代器,生成器)
  -flask:路由、裝飾器

  -認證

  -csrf

三、面向對象

  -繼承、封裝、多態(簡單描述)

 -雙下劃線:

    __mro__ wtform中 FormMeta中繼承類的優先級

     __dict__    

    __new__ ,實例化可是沒有給當前對象
        wtforms,字段實例化時返回:不是StringField,而是UnboundField

       rest frawork many=Turn  中的序列化

    __call__
       flask 請求的入口app.run()

        字段生成標籤時:字段.__str__ => 字段.__call__ => 插件.__call__

    

       __iter__ 循環對象是,自定義__iter__

        wtforms中BaseForm中循環全部字段時定義了__iter__

    metaclass        - 做用:用於指定當前類使用哪一個類來建立        - 場景:在類建立以前定製操做        示例:wtforms中,對字段進行排序。

相關文章
相關標籤/搜索