溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Flask核心機(jī)制之上下文管理的示例分析

發(fā)布時(shí)間:2021-08-21 14:22:31 來源:億速云 閱讀:95 作者:小新 欄目:開發(fā)技術(shù)

小編給大家分享一下Flask核心機(jī)制之上下文管理的示例分析,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!

知識(shí)儲(chǔ)備

threadlocal

在多線程中,線程間的數(shù)據(jù)是共享的, 但是每個(gè)線程想要有自己的數(shù)據(jù)該怎么實(shí)現(xiàn)? python中的threading.local對(duì)象已經(jīng)實(shí)現(xiàn),其原理是利用線程的唯一標(biāo)識(shí)作為key,數(shù)據(jù)作為value來保存其自己的數(shù)據(jù),以下是demo演示了多個(gè)線程同時(shí)修改同一變量的值的結(jié)果:

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

import threading
import time
values=threading.local()

def run(arg):
  values.num=arg #修改threading.local對(duì)象的name數(shù)據(jù)
  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()

結(jié)果:
run thread0 0
run thread1 1
run thread2 2

結(jié)果說明:

從結(jié)果中可以看到,values.num的值是不同的,按照普通線程理解因?yàn)橛衧leep存在,在每個(gè)線程最后打印values.num時(shí)候值應(yīng)該都是2,但是正是因?yàn)閠hreading.local對(duì)象內(nèi)部會(huì)為每個(gè)線程開辟一個(gè)內(nèi)存空間,從而使得每個(gè)線程都有自己的單獨(dú)數(shù)據(jù),所以每個(gè)線程修改的是自己的數(shù)據(jù)(內(nèi)部實(shí)現(xiàn)為字典),打印結(jié)果才不一樣。

有了以上的設(shè)計(jì)思想,我們可以自己定義類似于thread.local類,為了支持協(xié)程,將其唯一標(biāo)識(shí)改為協(xié)程的唯一標(biāo)識(shí),其實(shí)這已經(jīng)及其接近flask中的Local類了(后續(xù)在進(jìn)行說明):

try:
  from greenlet import getcurrent as get_ident # 攜程唯一標(biāo)識(shí)
except ImportError:
  try:
    from thread import get_ident
  except ImportError:
    from _thread import get_ident # 線程唯一標(biāo)識(shí)


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__() # 獲取當(dāng)前線程或協(xié)程的唯一標(biāo)識(shí)
    data = self.storage.get(ident)
    if not data: # 當(dāng)前線程沒有數(shù)據(jù)
      data = {key: value} # 創(chuàng)建數(shù)據(jù)
    else: # 當(dāng)前已經(jīng)有數(shù)據(jù)
      data[key] = value

    self.storage[ident] = data # 最后為當(dāng)前線程設(shè)置其標(biāo)識(shí)對(duì)應(yīng)的數(shù)據(jù)

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

functools.partial

partial函數(shù)是工具包的一個(gè)不常用函數(shù),其作用是給函數(shù)傳遞參數(shù),同時(shí)返回的也是這個(gè)函數(shù),但是這個(gè)函數(shù)的已經(jīng)帶了參數(shù)了,示例:

from functools import partial

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

new_fun=partial(func,1,2) #生成新的函數(shù),該函數(shù)中已經(jīng)有一個(gè)參數(shù)
new_fun(3)

結(jié)果:
1 2 3

在以上示例中,new_func是由func生成的,它已經(jīng)參數(shù)1,2了,只需要傳遞3即可運(yùn)行。

werkzeug

werkzeug是一個(gè)實(shí)現(xiàn)了wsgi協(xié)議的模塊,用官方語言介紹:Werkzeug is a WSGI utility library for Python. It's widely used and BSD licensed。為什么會(huì)提到它呢,這是因?yàn)閒lask內(nèi)部使用的wsgi模塊就是werkzeug,以下是一個(gè)示例(如果你了解wsgi協(xié)議的應(yīng)該不用過多介紹):

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是一個(gè)可調(diào)用的對(duì)象也可以是帶有__call__方法的對(duì)象,在run_simple內(nèi)部執(zhí)行application(),也就是在源碼的execute(self.server.app)中執(zhí)行,這里你只需要run_simple會(huì)執(zhí)行第三個(gè)參數(shù)加括號(hào)。

上下文管理

在說請求上下文之前先看一個(gè)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實(shí)例化的對(duì)象,所以執(zhí)行的是Flask類中的run方法,而在該改方法中又執(zhí)行了run_simple方法,以下是run方法部分源碼摘抄(其中self就是app對(duì)象):

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中會(huì)執(zhí)行app(environ, start_response),參考werkzeug的源碼,源碼會(huì)執(zhí)行app(environ, start_response)也就是執(zhí)行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__方法中又調(diào)用了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 當(dāng)前app名稱
  #ctx.request request對(duì)象,由app.request_class(environ)生成
  #ctx.session session 相關(guān)信息
  ctx = self.request_context(environ) 
  error = None
  try:
    try:
      ctx.push()
      #push數(shù)據(jù)到local,此時(shí)push的數(shù)據(jù)分請求上線文和應(yīng)用上下文
      # 將ctx通過Localstack添加到local中
      # app_ctx是APPContext對(duì)象
      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)調(diào)用request_context實(shí)例化RequestContext對(duì)象,以下是RequestContext類的構(gòu)造方法:

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

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

第二句:ctx.push(), 調(diào)用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 #獲取應(yīng)用上線文,一開始為none
  if app_ctx is None or app_ctx.app != self.app:
    # 創(chuàng)建APPContext(self)對(duì)象,app_ctx=APPContext(self)
    # 包含app_ctx.app ,當(dāng)前app對(duì)象
    # 包含app_ctx.g , g可以看作是一個(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對(duì)象,其中包含了請求相關(guān)的所有數(shù)據(jù)
  _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)

到了這里可以看到,相關(guān)注解已經(jīng)標(biāo)注,flask內(nèi)部將上下文分為了app_ctx(應(yīng)用上下文)和_request_ctx(請求上下文),并分別用來兩個(gè)LocalStack()來存放各自的數(shù)據(jù)(以下會(huì)用request_ctx說明,當(dāng)然app_ctx也一樣),其中app_ctx包含app、url_adapter一下是app_ctx構(gòu)造方法:

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

然后分別執(zhí)行app_ctx.push()方法和_request_ctx_stack.push(self)方法,將數(shù)據(jù)push到stack上,_request_ctx_stack.push(self),而_request_ctx_stack是一個(gè)LocalStack對(duì)象,是一個(gè)全局對(duì)象,具體路徑在flask.globals,以下是其push方法:

def push(self, obj):
  """Pushes a new item to the stack"""
  #找_local對(duì)象中是否有stack,沒有設(shè)置rv和_local.stack都為[]
  rv = getattr(self._local, 'stack', None)
  if rv is None:
    self._local.stack = rv = []
    # 執(zhí)行Local對(duì)象的__setattr__方法,等價(jià)于a=[],rv=a, self._local.stack =a
    #創(chuàng)建字典,類似于storage={'唯一標(biāo)識(shí)':{'stack':[]}}
  rv.append(obj)
    #列表中追加請求相關(guān)所有數(shù)據(jù)也就是storage={'唯一標(biāo)識(shí)':{'stack':[RequestContext對(duì)象,]}}
  return rv

以上代碼中的self._local是一個(gè)Local()對(duì)象源碼定義如下,也就是用于存儲(chǔ)每次請求的數(shù)據(jù),和我們剛開始定義的local及其相似,這也是為什么要先提及下threadlocal。

Local()

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()

到這里我們知道了,當(dāng)執(zhí)行ctx.push()時(shí),local對(duì)象中已經(jīng)有數(shù)據(jù)了,接著開始執(zhí)行self.full_dispatch_request(),也就是開始執(zhí)行視圖函數(shù),以下是源碼摘抄:

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)

在改方法中調(diào)用self.preprocess_request(),用于執(zhí)行所有被before_request裝飾器裝飾的函數(shù),從源碼總可以看到如果該函數(shù)有返回,則不會(huì)執(zhí)行self.dispatch_request()也就是視圖函數(shù),

執(zhí)行完畢之后調(diào)用self.dispatch_request()根據(jù)路由匹配執(zhí)行視圖函數(shù),然后響應(yīng)最后調(diào)用ctx.auto_pop(error)將stack中的數(shù)據(jù)刪除,此時(shí)完成一次請求。

全局對(duì)象request、g、session

在了解完flask的上下文管理時(shí)候,我們在視圖函數(shù)中使用的request實(shí)際上是一個(gè)全局變量對(duì)象,當(dāng)然還有g(shù)、session這里以request為例子,它是一個(gè)LocalProxy對(duì)象,以下是源碼片段:

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

當(dāng)我們使用request.path時(shí)候?qū)嶋H上是調(diào)用是其__getattr__方法即LocalProxy對(duì)象的__getattr__方法,我們先來看看LocalProxy對(duì)象實(shí)例化的參數(shù):

def __init__(self, local, name=None):
  #local是傳入的函數(shù),該句等價(jià)于self.__local=local,_類名__字段強(qiáng)行設(shè)置私有字段值
  #如果是requst則函數(shù)就是partial(_lookup_req_object, 'request')
  object.__setattr__(self, '_LocalProxy__local', local)
  object.__setattr__(self, '__name__', name) #開始的時(shí)候設(shè)置__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)

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

def __getattr__(self, name):
  #以獲取request.method 為例子,此時(shí)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)

在以上方法中會(huì)調(diào)用self._get_current_object()方法,而_get_current_object()方法中會(huì)調(diào)用self.__local()也就是帶參數(shù)request參數(shù)的 _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)對(duì)象,里面含有request、session 等
  if top is None:
    raise RuntimeError(_request_ctx_err_msg)
  return getattr(top, name) #到RequestContext(ctx)中獲取那么為request的值

在源碼中很簡單無非就是利用_request_ctx_stack(也就是LocalStack對(duì)象)的top屬性返回stack中的ctx,在通過反射獲取request,最后返回ctx.request。以上是整個(gè)flask的上下文核心機(jī)制,與其相似的全局對(duì)象有如下(session、g):

# context locals
_request_ctx_stack = LocalStack() #LocalStack()包含pop、push方法以及Local對(duì)象,上下文通過該對(duì)象push和pop
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, 'request')) #reuqest是LocalProxy的對(duì)象,設(shè)置和獲取request對(duì)象中的屬性通過LocalProxy定義的各種雙下劃線實(shí)現(xiàn)
session = LocalProxy(partial(_lookup_req_object, 'session'))
g = LocalProxy(partial(_lookup_app_object, 'g'))

技巧應(yīng)用

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

from flask import Flask,_request_ctx_stack

app=Flask(__name__)

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

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

以上是“Flask核心機(jī)制之上下文管理的示例分析”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對(duì)大家有所幫助,如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI