Flask实践Python-Flask

Flask 请求处理流程(二):Context 对象

2018-12-25  本文已影响10人  Uchen

上下文一直是计算机中难理解的概念,在知乎的一个问题下面有个很通俗易懂的回答:

每一段程序都有很多外部变量。只有像 Add 这种简单的函数才是没有外部变量的。一旦你的一段程序有了外部变量,这段程序就不完整,不能独立运行。你为了使他们运行,就要给所有的外部变量一个一个写一些值进去。这些值的集合就叫上下文(context)
–– vzch

比如,在 flask 中,视图函数(views.py)需要知道它执行的请求信息(请求的 url,参数,方法等)以及应用信息(应用中初始化的数据库等),才能够正确运行和给出相应的返回。

最直观地做法是把这些信息封装成一个对象,作为参数传递给视图函数。但是这样的话,所有的视图函数都需要添加对应的参数,即使该函数内部并没有使用到它。

flask 的做法是把这些信息作为类似**全局变量的东西**,视图函数需要的时候,可以使用 from flask import request 获取。但是这些对象和全局变量不同的是——它们必须是动态的,因为在多线程或者多协程的情况下,每个线程或者协程获取的都是自己独特的对象,不会互相干扰。

Context(上下文)


Request Context 请求上下文

App Context 程序上下文

生命周期

处理流程

  1. 创建上下文
  2. 入栈
  3. 请求分发
  4. 上下文对象出栈
  5. 响应WSGI
处理流程
第一步:创建上下文

Flask 根据 WSGI Server 封装的请求信息(存放在environ),新建 RequestContext 对象 和 AppContext 对象

# 声明对象
# LocalStack  LocalProxy 都由 Werkzeug 提供
# 我们不深究他的细节,那又是另外一个故事了,我们只需知道他的作用就行了
# LocalStack 是栈结构,可以将对象推入、弹出
# 也可以快速拿到栈顶对象。当然,所有的修改都只在本线程可见。
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()

# 如果调用一个 LocalStack 实例, 能返回一个 LocalProxy 对象
# 这个对象始终指向这个 LocalStack 实例的栈顶元素。
# 如果栈顶元素不存在,访问这个 LocalProxy 的时候会抛出 RuntimeError 异常
# LocalProxy 对象你只需暂时理解为栈里面的元素即可了
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'))

部分 RequestContext 源码

# RequestContext
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 = None

部分 AppContext 源码

#AppContext
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()    
        self._refcnt = 0

这里需要注意的是,RequestContext 在初始化的时候,当前 Flask 的实例作为参数被传进来。虽然每次的请求处理都会创建一个 RequestContext 对象,但是每一次传入的 app 参数却是同一个。通过这个机制,可以使得:

由同一个 Flask 实例所创建的 RequestContext,其成员变量 app 都是同一个 Flask 实例对象 。实现了多个 RequestContext 对应同一个 current_app 的目的。

第二步:入栈

RequestContext 对象 push 进 _request_ctx_stack 里面。
在这次请求期间,访问 request、session 对象将指向这个栈的栈顶元素

class RequestContext(object):
    def push(self):   
        ....
        _app_ctx_stack.push(self)   
        appcontext_pushed.send(self.app)

AppContext 对象 push 进 _app_ctx_stack里面。
在这次请求期间,访问 g 对象将指向这个栈的栈顶元素

class AppContext(object):
    def push(self):   
        ....
        _request_ctx_stack.push(self)
第三步:请求分发
response = self.full_dispatch_request()

Flask 将调用 full_dispatch_request 函数进行请求的分发,之所以不用给参数,是因为我们可以通过 request 对象获得这次请求的信息。full_dispatch_request 将根据请求的 url 找到对应的蓝本里面的视图函数,并生成一个 response 对象。注意的是,在请求之外的时间,访问 request 对象是无效的,因为 request 对象依赖请求期间的 _request_ctx_stack 栈。

第四步:上下文对象出栈

这次 HTTP 的响应已经生成了,就不需要两个上下文对象了。分别将两个上下文对象出栈,为下一次的 HTTP 请求做出准备。

第五步:响应 WSGI

调用 Response 对象,向 WSGI Server 返回其结果作为 HTTP 正文。Response 对象是一个可调用对象,当调用发生时,将首先执行 WSGI 服务器传入的 start_response() 函数,发送状态码和 HTTP 报文头。

最后再来看下 Flask 处理请求的 wsgi_app 函数:
# environ: WSGI Server 封装的 HTTP 请求信息
# start_response: WSGI Server 提供的函数,调用可以发送状态码和 HTTP 报文头
def wsgi_app(self, environ, start_response):
    # 根据 environ 创建上下文
    ctx = self.request_context(environ)
    error = None
    try:
        try:
            # 把当前的 request context,app context 绑定到当前的 context
            ctx.push()
            # 根据请求的 URL,分发请求,经过视图函数处理后返回响应对象
            response = self.full_dispatch_request()
        except Exception as e:
            error = e
            response = self.make_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.py 源码


代码信息:

总共代码行数:468 行,
有效行数:184 行,
注释:187 行,
空行:97 行

从这里可以看出 flask 是如此简约而不简单的微框架,可见一斑。

三个 class,分别为:

class AppContext(object):
    """The application context binds an application object implicitly
    to the current thread or greenlet, similar to how the
    :class:`RequestContext` binds request information.  The application
    context is also implicitly created if a request context is created
    but the application is not on top of the individual application
    context.
    """

    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
class RequestContext(object):
    """The request context contains all request relevant information.  It is
    created at the beginning of the request and pushed to the
    `_request_ctx_stack` and removed at the end of it.  It will create the
    URL adapter and request object for the WSGI environment provided.

    Do not attempt to use this class directly, instead use
    :meth:`~flask.Flask.test_request_context` and
    :meth:`~flask.Flask.request_context` to create this object.

    When the request context is popped, it will evaluate all the
    functions registered on the application for teardown execution
    (:meth:`~flask.Flask.teardown_request`).

    The request context is automatically popped at the end of the request
    for you.  In debug mode the request context is kept around if
    exceptions happen so that interactive debuggers have a chance to
    introspect the data.  With 0.4 this can also be forced for requests
    that did not fail and outside of ``DEBUG`` mode.  By setting
    ``'flask._preserve_context'`` to ``True`` on the WSGI environment the
    context will not pop itself at the end of the request.  This is used by
    the :meth:`~flask.Flask.test_client` for example to implement the
    deferred cleanup functionality.

    You might find this helpful for unittests where you need the
    information from the context local around for a little longer.  Make
    sure to properly :meth:`~werkzeug.LocalStack.pop` the stack yourself in
    that situation, otherwise your unittests will leak memory.
    """

    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

        # 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 = []

        # indicator if the context was preserved.  Next time another context
        # is pushed the preserved context is popped.
        self.preserved = False

        # remembers the exception for pop if there is one in case the context
        # preservation kicks in.
        self._preserved_exc = None

        # Functions that should be executed after the request on the response
        # object.  These will be called before the regular "after_request"
        # functions.
        self._after_request_functions = []

        if self.url_adapter is not None:
            self.match_request()

注: A plain object:非常轻量,主要用来归集一些属性方便访问。

四个 def,分别为:

@app.route('/')
def index():
    @after_this_request
    def add_header(response):
        response.headers['X-Foo'] = 'Parachute'  ## 响应头中插入一个值
        return response
    return 'Hello World!'
import gevent
from flask import copy_current_request_context

@app.route('/')
def index():
    @copy_current_request_context
    def do_some_work():
        # do some work here, it can access flask.request or
        # flask.session like you would otherwise in the view function.
        ...
    gevent.spawn(do_some_work)
        return 'Regular response'

参考:

  1. https://www.jianshu.com/p/2a2407f66438
  2. https://zhuanlan.zhihu.com/p/32457833
  3. https://blog.tonyseek.com/post/the-context-mechanism-of-flask/
上一篇下一篇

猜你喜欢

热点阅读