一、前言

  了解过flask的python开发者想必都知道flask中核心机制莫过于上下文管理,当然学习flask如果不了解其中的处理流程,可能在很多问题上不能得到解决,当然我在写本篇文章之前也看到了很多博文有关于对flask上下文管理的剖析都非常到位,当然为了学习flask我也把对flask上下文理解写下来供自己参考,也希望对其他人有所帮助。

二、知识储备

threadlocal

  在多线程中,线程间的数据是共享的, 但是每个线程想要有自己的数据该怎么实现? python中的threading.local对象已经实现,其原理是利用线程的唯一标识作为key,数据作为value来保存其自己的数据,以下是demo演示了多个线程同时修改同一变量的值的结果:

  1. #!/usr/bin/env python3
  2. # -*- coding:utf-8 -*-
  3. # Author:wd
  4.  
  5. import threading
  6. import time
  7. values=threading.local()
  8.  
  9. def run(arg):
  10. values.num=arg #修改threading.local对象的name数据
  11. time.sleep(1)
  12. print(threading.current_thread().name,values.num) #打印values.num
  13.  
  14. for i in range(3):
  15. th = threading.Thread(target=run, args=(i,), name='run thread%s' % i)
  16. th.start()
  17.  
  18. 结果:
  19. run thread0 0
  20. run thread1 1
  21. run thread2 2

结果说明:

从结果中可以看到,values.num的值是不同的,按照普通线程理解因为有sleep存在,在每个线程最后打印values.num时候值应该都是2,但是正是因为threading.local对象内部会为每个线程开辟一个内存空间,从而使得每个线程都有自己的单独数据,所以每个线程修改的是自己的数据(内部实现为字典),打印结果才不一样。

有了以上的设计思想,我们可以自己定义类似于thread.local类,为了支持协程,将其唯一标识改为协程的唯一标识,其实这已经及其接近flask中的Local类了(后续在进行说明):

  1. try:
  2. from greenlet import getcurrent as get_ident # 携程唯一标识
  3. except ImportError:
  4. try:
  5. from thread import get_ident
  6. except ImportError:
  7. from _thread import get_ident # 线程唯一标识
  8.  
  9. class Local(object):
  10. def __init__(self):
  11. object.__setattr__(self, 'storage', dict()) # 防止self.xxx 递归
  12. object.__setattr__(self, '__get_ident__', get_ident)
  13.  
  14. def __setattr__(self, key, value):
  15. ident = self.__get_ident__() # 获取当前线程或协程的唯一标识
  16. data = self.storage.get(ident)
  17. if not data: # 当前线程没有数据
  18. data = {key: value} # 创建数据
  19. else: # 当前已经有数据
  20. data[key] = value
  21.  
  22. self.storage[ident] = data # 最后为当前线程设置其标识对应的数据
  23.  
  24. def __getattr__(self, name):
  25. try:
  26. return self.storage[self.__get_ident__()].get(name) # 返回name所对应的值
  27. except KeyError:
  28. raise AttributeError(name)

functools.partial

  partial函数是工具包的一个不常用函数,其作用是给函数传递参数,同时返回的也是这个函数,但是这个函数的已经带了参数了,示例:

  1. from functools import partial
  2.  
  3. def func(x,y,z):
  4. print(x,y,z)
  5.  
  6. new_fun=partial(func,1,2) #生成新的函数,该函数中已经有一个参数
  7. new_fun(3)
  8.  
  9. 结果:
  10. 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协议的应该不用过多介绍):

  1. from werkzeug.wrappers import Request, Response
  2.  
  3. @Request.application
  4. def application(request):
  5. return Response('Hello World!')
  6.  
  7. if __name__ == '__main__':
  8. from werkzeug.serving import run_simple
  9. run_simple('localhost', 4000, application)

在示例中application是一个可调用的对象也可以是带有__call__方法的对象,在run_simple内部执行application(),也就是在源码的execute(self.server.app)中执行,这里你只需要run_simple会执行第三个参数加括号。

三、源码剖析

上下文管理

   在说请求上下文之前先看一个flask的hell world示例:

  1. from flask import Flask
  2.  
  3. app=Flask(__name__)
  4. @app.route("/")
  5. def hello():
  6. return 'hello world'
  7.  
  8. if __name__=='__main__':
  9. app.run()

在以上示例中,app.run是请求的入口,而app是Flask实例化的对象,所以执行的是Flask类中的run方法,而在该改方法中又执行了run_simple方法,以下是run方法部分源码摘抄(其中self就是app对象):
  1. from werkzeug.serving import run_simple
  2.  
  3. try:
  4. run_simple(host, port, self, **options)
  5. finally:
  6. # reset the first request information if the development server
  7. # reset normally. This makes it possible to restart the server
  8. # without reloader and that stuff from an interactive shell.
  9. self._got_first_request = False

在run_simple中会执行app(environ, start_response),参考werkzeug的源码,源码会执行app(environ, start_response)也就是执行app的__call__方法,以下是__call__方法源码摘抄:
  1. def __call__(self, environ, start_response):
  2. """The WSGI server calls the Flask application object as the
  3. WSGI application. This calls :meth:`wsgi_app` which can be
  4. wrapped to applying middleware."""
  5. return self.wsgi_app(environ, start_response)

__call__方法中又调用了wsgi_app方法,该方法也就是flask的核心所在,下面是方法摘抄:

  1. def wsgi_app(self, environ, start_response):
  2. """The actual WSGI application. This is not implemented in
  3. :meth:`__call__` so that middlewares can be applied without
  4. losing a reference to the app object. Instead of doing this::
  5.  
  6. app = MyMiddleware(app)
  7.  
  8. It's a better idea to do this instead::
  9.  
  10. app.wsgi_app = MyMiddleware(app.wsgi_app)
  11.  
  12. Then you still have the original application object around and
  13. can continue to call methods on it.
  14.  
  15. .. versionchanged:: 0.7
  16. Teardown events for the request and app contexts are called
  17. even if an unhandled error occurs. Other events may not be
  18. called depending on when an error occurs during dispatch.
  19. See :ref:`callbacks-and-errors`.
  20.  
  21. :param environ: A WSGI environment.
  22. :param start_response: A callable accepting a status code,
  23. a list of headers, and an optional exception context to
  24. start the response.
  25. """
  26. #ctx.app 当前app名称
  27. #ctx.request request对象,由app.request_class(environ)生成
  28. #ctx.session session 相关信息
  29. ctx = self.request_context(environ)
  30. error = None
  31. try:
  32. try:
  33. ctx.push()
  34. #push数据到local,此时push的数据分请求上线文和应用上下文
  35. # 将ctx通过Localstack添加到local中
  36. # app_ctx是APPContext对象
  37. response = self.full_dispatch_request()
  38. except Exception as e:
  39. error = e
  40. response = self.handle_exception(e)
  41. except:
  42. error = sys.exc_info()[1]
  43. raise
  44. return response(environ, start_response)
  45. finally:
  46. if self.should_ignore_error(error):
  47. error = None
  48. ctx.auto_pop(error)

第一句:ctx = self.request_context(environ)调用request_context实例化RequestContext对象,以下是RequestContext类的构造方法:

  1. def __init__(self, app, environ, request=None):
  2. self.app = app
  3. if request is None:
  4. request = app.request_class(environ)
  5. self.request = request
  6. self.url_adapter = app.create_url_adapter(self.request)
  7. self.flashes = None
  8. 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方法,以下是源码摘抄:
  1. def push(self):
  2. """Binds the request context to the current context."""
  3. # If an exception occurs in debug mode or if context preservation is
  4. # activated under exception situations exactly one context stays
  5. # on the stack. The rationale is that you want to access that
  6. # information under debug situations. However if someone forgets to
  7. # pop that context again we want to make sure that on the next push
  8. # it's invalidated, otherwise we run at risk that something leaks
  9. # memory. This is usually only a problem in test suite since this
  10. # functionality is not active in production environments.
  11. top = _request_ctx_stack.top
  12. if top is not None and top.preserved:
  13. top.pop(top._preserved_exc)
  14.  
  15. # Before we push the request context we have to ensure that there
  16. # is an application context.
  17. app_ctx = _app_ctx_stack.top #获取应用上线文,一开始为none
  18. if app_ctx is None or app_ctx.app != self.app:
  19. # 创建APPContext(self)对象,app_ctx=APPContext(self)
  20. # 包含app_ctx.app ,当前app对象
  21. # 包含app_ctx.g , g可以看作是一个字典用来保存一个请求周期需要保存的值
  22. app_ctx = self.app.app_context()
  23. app_ctx.push()
  24. self._implicit_app_ctx_stack.append(app_ctx)
  25. else:
  26. self._implicit_app_ctx_stack.append(None)
  27.  
  28. if hasattr(sys, 'exc_clear'):
  29. sys.exc_clear()
  30. #self 是RequestContext对象,其中包含了请求相关的所有数据
  31. _request_ctx_stack.push(self)
  32.  
  33. # Open the session at the moment that the request context is available.
  34. # This allows a custom open_session method to use the request context.
  35. # Only open a new session if this is the first time the request was
  36. # pushed, otherwise stream_with_context loses the session.
  37. if self.session is None:
  38. session_interface = self.app.session_interface # 获取session信息
  39. self.session = session_interface.open_session(
  40. self.app, self.request
  41. )
  42.  
  43. if self.session is None:
  44. 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构造方法:
  1. def __init__(self, app):
  2. self.app = app
  3. self.url_adapter = app.create_url_adapter(None)
  4. self.g = app.app_ctx_globals_class()
  5.  
  6. # Like request context, app contexts can be pushed multiple times
  7. # but there a basic "refcount" is enough to track them.
  8. 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方法: 
  1. def push(self, obj):
  2. """Pushes a new item to the stack"""
  3. #找_local对象中是否有stack,没有设置rv和_local.stack都为[]
  4. rv = getattr(self._local, 'stack', None)
  5. if rv is None:
  6. self._local.stack = rv = []
  7. # 执行Local对象的__setattr__方法,等价于a=[],rv=a, self._local.stack =a
  8. #创建字典,类似于storage={'唯一标识':{'stack':[]}}
  9. rv.append(obj)
  10. #列表中追加请求相关所有数据也就是storage={'唯一标识':{'stack':[RequestContext对象,]}}
  11. return rv

以上代码中的self._local是一个Local()对象源码定义如下,也就是用于存储每次请求的数据,和我们刚开始定义的local及其相似,这也是为什么要先提及下threadlocal。
  1. class Local(object):
  2. __slots__ = ('__storage__', '__ident_func__')
  3.  
  4. def __init__(self):
  5. object.__setattr__(self, '__storage__', {})
  6. object.__setattr__(self, '__ident_func__', get_ident)
  7.  
  8. def __iter__(self):
  9. return iter(self.__storage__.items())
  10.  
  11. def __call__(self, proxy):
  12. """Create a proxy for a name."""
  13. return LocalProxy(self, proxy)
  14.  
  15. def __release_local__(self):
  16. self.__storage__.pop(self.__ident_func__(), None)
  17.  
  18. def __getattr__(self, name):
  19. try:
  20. return self.__storage__[self.__ident_func__()][name]
  21. except KeyError:
  22. raise AttributeError(name)
  23.  
  24. def __setattr__(self, name, value):
  25. ident = self.__ident_func__()
  26. storage = self.__storage__
  27. try:
  28. storage[ident][name] = value
  29. except KeyError:
  30. storage[ident] = {name: value}
  31.  
  32. def __delattr__(self, name):
  33. try:
  34. del self.__storage__[self.__ident_func__()][name]
  35. except KeyError:
  36. raise AttributeError(name)

Local()

到这里我们知道了,当执行ctx.push()时,local对象中已经有数据了,接着开始执行self.full_dispatch_request(),也就是开始执行视图函数,以下是源码摘抄:
  1. def full_dispatch_request(self):
  2. """Dispatches the request and on top of that performs request
  3. pre and postprocessing as well as HTTP exception catching and
  4. error handling.
  5.  
  6. .. versionadded:: 0.7
  7. """
  8. self.try_trigger_before_first_request_functions()
  9. try:
  10. request_started.send(self)
  11. rv = self.preprocess_request()
  12. if rv is None:
  13. rv = self.dispatch_request()
  14. except Exception as e:
  15. rv = self.handle_user_exception(e)
  16. 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对象,以下是源码片段:

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

当我们使用request.path时候实际上是调用是其__getattr__方法即LocalProxy对象的__getattr__方法,我们先来看看LocalProxy对象实例化的参数:

  1. def __init__(self, local, name=None):
  2. #local是传入的函数,该句等价于self.__local=local,_类名__字段强行设置私有字段值
  3. #如果是requst则函数就是partial(_lookup_req_object, 'request')
  4. object.__setattr__(self, '_LocalProxy__local', local)
  5. object.__setattr__(self, '__name__', name) #开始的时候设置__name__的值为None
  6. if callable(local) and not hasattr(local, '__release_local__'):
  7. # "local" is a callable that is not an instance of Local or
  8. # LocalManager: mark it as a wrapped function.
  9. object.__setattr__(self, '__wrapped__', local)

在源码中实例化时候传递的是partial(_lookup_req_object, 'request')函数作为参数,也就是self.__local=该函数,partial参数也就是我们之前提到的partial函数,作用是传递参数,此时为_lookup_req_object函数传递request参数,这个在看看其__getattr__方法:

  1. def __getattr__(self, name):
  2. #以获取request.method 为例子,此时name=method
  3. if name == '__members__':
  4. return dir(self._get_current_object())
  5. #self._get_current_object()返回的是ctx.request,再从ctx.request获取method (ctx.request.method)
  6. 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,以下是源码摘抄:

  1. def _lookup_req_object(name):
  2. #以name=request为列
  3. top = _request_ctx_stack.top
  4. # top是就是RequestContext(ctx)对象,里面含有request、session 等
  5. if top is None:
  6. raise RuntimeError(_request_ctx_err_msg)
  7. return getattr(top, name) #到RequestContext(ctx)中获取那么为request的值

在源码中很简单无非就是利用_request_ctx_stack(也就是LocalStack对象)的top属性返回stack中的ctx,在通过反射获取request,最后返回ctx.request。以上是整个flask的上下文核心机制,与其相似的全局对象有如下(session、g):
  1. # context locals
  2. _request_ctx_stack = LocalStack() #LocalStack()包含pop、push方法以及Local对象,上下文通过该对象push和pop
  3. _app_ctx_stack = LocalStack()
  4. current_app = LocalProxy(_find_app)
  5. request = LocalProxy(partial(_lookup_req_object, 'request’)) #reuqest是LocalProxy的对象,设置和获取request对象中的属性通过LocalProxy定义的各种双下划线实现
  6. session = LocalProxy(partial(_lookup_req_object, 'session'))
  7. g = LocalProxy(partial(_lookup_app_object, 'g'))

技巧应用

利用flask的上下文处理机制我们获取上请求信息还可以使用如下方式:

  1. from flask import Flask,_request_ctx_stack
  2.  
  3. app=Flask(__name__)
  4.  
  5. @app.route("/")
  6. def hello():
  7. print(_request_ctx_stack.top.request.method) #结果GET,等价于request.method
  8. return this is wd'
  9.  
  10. if __name__=='__main__':
  11. app.run()

  

Flask核心机制--上下文源码剖析的更多相关文章

  1. flask的请求上下文源码解读

    一.flask请求上下文源码解读 通过上篇源码分析( ---Flask中的CBV和上下文管理--- ),我们知道了有请求发来的时候就执行了app(Flask的实例化对象)的__call__方法,而__ ...

  2. Flask的上下文源码剖析

    先写一段Flask程序 from flask import Flask app = Flask(__name__) @app.route('/') def hello_world(): return ...

  3. Redis源码剖析

    Redis源码剖析和注释(一)---链表结构 Redis源码剖析和注释(二)--- 简单动态字符串 Redis源码剖析和注释(三)--- Redis 字典结构 Redis源码剖析和注释(四)--- 跳 ...

  4. Flask之 请求,应用 上下文源码解析

    什么是上下文? 每一段程序都有很多外部变量.只有像Add这种简单的函数才是没有外部变量的.一旦你的一段程序有了外部变量,这段程序就不完整,不能独立运行.你为了使他们运行,就要给所有的外部变量一个一个写 ...

  5. Flask请求和应用上下文源码分析

      flask的request和session设置方式比较新颖,如果没有这种方式,那么就只能通过参数的传递. flask是如何做的呢? 1:本地线程,保证即使是多个线程,自己的值也是互相隔离 1 im ...

  6. 《python解释器源码剖析》第12章--python虚拟机中的函数机制

    12.0 序 函数是任何一门编程语言都具备的基本元素,它可以将多个动作组合起来,一个函数代表了一系列的动作.当然在调用函数时,会干什么来着.对,要在运行时栈中创建栈帧,用于函数的执行. 在python ...

  7. Flask系列10-- Flask请求上下文源码分析

    总览 一.基础准备. 1. local类 对于一个类,实例化得到它的对象后,如果开启多个线程对它的属性进行操作,会发现数据时不安全的 import time from threading import ...

  8. Flask(4)- flask请求上下文源码解读、http聊天室单聊/群聊(基于gevent-websocket)

    一.flask请求上下文源码解读 通过上篇源码分析,我们知道了有请求发来的时候就执行了app(Flask的实例化对象)的__call__方法,而__call__方法返回了app的wsgi_app(en ...

  9. 转 Spring源码剖析——核心IOC容器原理

    Spring源码剖析——核心IOC容器原理 2016年08月05日 15:06:16 阅读数:8312 标签: spring源码ioc编程bean 更多 个人分类: Java https://blog ...

随机推荐

  1. vs2012碰到生成时报该错误:项目中不存在目标“GatherAllFilesToPublish”

    手头一个vs2010升级到vs2012后,web项目发布到本地目录时项目报错:“该项目中不存在目标“GatherAllFilesToPublish”” 通过谷歌大神的帮助,找到了解决方法.共享之. 原 ...

  2. mysql8 :客户端连接caching-sha2-password问题

    在安装mysql8的时候如果选择了密码加密,之后用客户端连接比如navicate,会提示客户端连接caching-sha2-password,是由于客户端不支持这种插件,可以通过如下方式进行修改: # ...

  3. 使用wxpy自动发送微信消息

    思路整理:1.进入心灵鸡汤网页,使用python获取心灵鸡汤内容 2.登陆微信,找到需要发送的朋友 3.发送获取的内容 1.获取心灵鸡汤的内容 如下图,获取第一条鸡汤 实现如下: 2.登陆微信,搜索朋 ...

  4. [Winform-WebBrowser]-在html页面中js调用winForm类方法

    在winform项目中嵌入了网页,想通过html页面调用后台方法,如何实现呢?其实很简单,主要有三部: 1.在被调用方法类上加上[ComVisible(true)]标签,意思就是当前类可以com组件的 ...

  5. MySql数据库基础笔记(一)

    一.表与库的概念 数据库管理数据,它是以什么样的形式组成的? 生活中的表---->表 table多行多列,传统的数据库都是这样的;声明了表头,一个表创建好了,剩下的就是往表中添加数据 多张表放在 ...

  6. php报错 【 Call to undefined function imagecreatetruecolor()】

    刚才在写验证码的时候,发现报错,然后排查分析了一下,原来是所用的php版本(PHP/5.3.13)没有开启此扩展功能. 进入php.ini 找到extension=php_gd2.dll ,将其前面的 ...

  7. lvm管理卷之缩减卷大小

    最近刚刚装好了一个系统,但是因为没有分好区,导致home分区过大,所以想把home分区的一大半移动到根分区里面. 1.先说一下我的环境. 安装的是centos6版本的系统,使用的默认文件系统是ext4 ...

  8. oracle+st_geometry

    最近因为性能的原因开始关注通过oracle和st_geometry直接操作数据库来解决实际业务问题.主要还是用到了“使用 SQL 处理 ST_Geometry”.对此,ESRI给出的帮助文档中的解释如 ...

  9. [NOIP 2015]运输计划-[树上差分+二分答案]-解题报告

    [NOIP 2015]运输计划 题面: A[NOIP2015 Day2]运输计划 时间限制 : 20000 MS 空间限制 : 262144 KB 问题描述 公元 2044 年,人类进入了宇宙纪元. ...

  10. BookStrap之模板继承

    模板继承 (extend) Django模版引擎中最强大也是最复杂的部分就是模版继承了.模版继承可以让您创建一个基本的“骨架”模版,它包含您站点中的全部元素,并且可以定义能够被子模版覆盖的 block ...