上下文是在Flask开发中的一个核心概念,本文将通过阅读源码分享下其原理和实现。

Flask系列文章

  1. Flask开发初探
  2. WSGI到底是什么
  3. Flask源码分析一:服务启动
  4. Flask路由内部实现原理
  5. Flask容器化部署原理与实现
  6. Flask权限管理

首先,什么是Flask中的上下文?

在Flask中,对一个请求进行处理时,视图函数一般都会需要请求参数、配置等对象,当然不能对每个请求都传参一层层到视图函数(这显然很不优雅嘛),为此,设计出了上下文机制(比如像我们经常会调用的request就是上下文变量)。

Flask中提供了两种上下文:

  1. 请求上下文:包括request和session,保存请求相关的信息
  2. 程序上下文:包括current_app和g,为了更好的分离程序的状态,应用起来更加灵活,方便调测等

这四个是上下文变量具体的作用是什么?

  1. request:封装客户端发送的请求报文数据
  2. session:用于记住请求之间的数据,通过签名的cookie实现,常用来记住用户登录状态
  3. current_app:指向处理请求的当前程序实例,比如获取配置,经常会用current_app.config
  4. g:当前请求中的全局变量,因为程序上下文的生命周期是伴随请求上下文产生和销毁的,所以每次请求都会重设。一般我会在结合钩子函数在请求处理前使用。

具体是怎么实现的呢?

上下文具体的实现文件:ctx.py

请求上下文对象通过RequestContext类实现,当Flask程序收到请求时,会在wsgi_app()中调用Flask.request_context(),实例化RequestContext()作为请求上下文对象,接着会通过push()方法将请求数据推入到请求上下文堆栈(LocalStack),然后通过full_dispatch_request对象执行视图函数,调用完成之后通过auto_pop方法来移除。所以,请求上下文的生命周期开始于调用wsgi_app()时,结束与响应生成之后。具体代码:

def wsgi_app(self, environ, start_response):

    ctx = self.request_context(environ)
error = None
try:
try:
ctx.push()
response = self.full_dispatch_request()
except Exception as e:
error = e
response = self.handle_exception(e)
except: # noqa: B001
error = sys.exc_info()[1]
raise
return response(environ, start_response)
finally:
if self.should_ignore_error(error):
error = None
ctx.auto_pop(error)

程序上下文对象通过AppContext类实现,程序上下文的创建方式有两种:

  1. 自动创建:在处理请求时,程序上下文会随着请求上下文一起被创建
  2. 手动创建:with语句

通过阅读源码,可以看到上面两个上下文对象的push和pop都是通过操作LocalStack对象实现的,那么,LocalStack是怎样实现的呢?

Werkzeug的LocalStack是栈结构,在 globals.py中定义:

_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()

具体的实现:

class LocalStack(object):

    def __init__(self):
self._local = Local() def __release_local__(self):
self._local.__release_local__() def _get__ident_func__(self):
return self._local.__ident_func__ def _set__ident_func__(self, value):
object.__setattr__(self._local, '__ident_func__', value)
__ident_func__ = property(_get__ident_func__, _set__ident_func__)
del _get__ident_func__, _set__ident_func__ def __call__(self):
def _lookup():
rv = self.top
if rv is None:
raise RuntimeError('object unbound')
return rv
return LocalProxy(_lookup) def push(self, obj):
"""Pushes a new item to the stack"""
rv = getattr(self._local, 'stack', None)
if rv is None:
self._local.stack = rv = []
rv.append(obj)
return rv def pop(self):
"""Removes the topmost item from the stack, will return the
old value or `None` if the stack was already empty.
"""
stack = getattr(self._local, 'stack', None)
if stack is None:
return None
elif len(stack) == 1:
release_local(self._local)
return stack[-1]
else:
return stack.pop() @property
def top(self):
"""The topmost item on the stack. If the stack is empty,
`None` is returned.
"""
try:
return self._local.stack[-1]
except (AttributeError, IndexError):
return None

可以看到:

  1. LocalStack实现了栈的push、pop和获取栈顶数据的top数据
  2. 整个类基于Local类,在构造函数中创建Local类的实例_local,数据是push到Werkzeug提供的Local类中
  3. 定义__call__方法,当实例被调用直接返回栈顶对象的Werkzeug提供的LocalProxy代理,即LocalProxy实例,所以,_request_ctx_stack_app_ctx_stack都是代理。

看到这里,就有以下问题:

Local类是怎样存储数据的呢?为啥需要存储到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):
__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构造函数中定义了两个属性:

  1. __storage__:用来保存每个线程的真实数据,对应的存储结构为->{线程ID:{name:value}}
  2. __ident_func__:通过get_ident()方法获取线程ID,可以看到优先会使用Greenlet获取协程ID,其次是thread模块的线程ID

Local类在保存数据的同时,记录对应的线程ID,获取数据时根据当前线程的id即可获取到对应数据,这样就保证了全局使用的上下文对象不会在多个线程中产生混乱,保证了每个线程中上下文对象的独立和准确。

可以看到,Local类实例被调用时也同样的被包装成了一个LocalProxy代理,为什么要用LocalProxy代理?

代理是一种设计模式,通过创建一个代理对象来操作实际对象,简单理解就是使用一个中间人来转发操作,Flask上下文处理为什么需要它?

看下代码实现:

@implements_bool
class LocalProxy(object):
__slots__ = ('__local', '__dict__', '__name__', '__wrapped__') def __init__(self, local, name=None):
object.__setattr__(self, '_LocalProxy__local', local)
object.__setattr__(self, '__name__', name)
if callable(local) and not hasattr(local, '__release_local__'):
object.__setattr__(self, '__wrapped__', local) def _get_current_object(self):
"""
获取被代理的实际对象
"""
if not hasattr(self.__local, '__release_local__'):
return self.__local()
try:
return getattr(self.__local, self.__name__)
except AttributeError:
raise RuntimeError('no object bound to %s' % self.__name__) @property
def __dict__(self):
try:
return self._get_current_object().__dict__
except RuntimeError:
raise AttributeError('__dict__') def __repr__(self):
try:
obj = self._get_current_object()
except RuntimeError:
return '<%s unbound>' % self.__class__.__name__
return repr(obj) def __bool__(self):
try:
return bool(self._get_current_object())
except RuntimeError:
return False def __unicode__(self):
try:
return unicode(self._get_current_object()) # noqa
except RuntimeError:
return repr(self) def __dir__(self):
try:
return dir(self._get_current_object())
except RuntimeError:
return [] def __getattr__(self, name):
if name == '__members__':
return dir(self._get_current_object())
return getattr(self._get_current_object(), name) def __setitem__(self, key, value):
self._get_current_object()[key] = value def __delitem__(self, key):
del self._get_current_object()[key]
...

通过__getattr__()__setitem__()__delitem__会动态的更新实例对象。

再结合上下文对象的调用:

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

我们可以很明确的看到:因为上下文的推送和删除是动态进行的,所以使用代理来动态的获取上下文对象。

以上,希望你对Flask上下文机制的原理有了清晰的认识。

详解Flask上下文的更多相关文章

  1. flask上下文详解

    一.前言 了解过flask的python开发者想必都知道flask中核心机制莫过于上下文管理,当然学习flask如果不了解其中的处理流程,可能在很多问题上不能得到解决,当然我在写本篇文章之前也看到了很 ...

  2. flask基础之session原理详解(十)

    前言 flask_session是flask框架实现session功能的一个插件,用来替代flask自带的session实现机制,flask默认的session信息保存在cookie中,不够安全和灵活 ...

  3. 块级格式化上下文(block formatting context)、浮动和绝对定位的工作原理详解

    CSS的可视化格式模型中具有一个非常重要地位的概念——定位方案.定位方案用以控制元素的布局,在CSS2.1中,有三种定位方案——普通流.浮动和绝对定位: 普通流:元素按照先后位置自上而下布局,inli ...

  4. Android菜单详解(四)——使用上下文菜单ContextMenu

    之前在<Android菜单详解(二)——创建并响应选项菜单>和<Android菜单详解(三)——SubMenu和IconMenu>中详细讲解了选项菜单,子菜单和图标菜单.今天接 ...

  5. Flask request 属性详解

    Flask request 属性详解 一.关于request在Flask的官方文档中是这样介绍request的:对于 Web 应用,与客户端发送给服务器的数据交互至关重要.在 Flask 中由全局的 ...

  6. Flask(4)- URL 组成部分详解

    URL Uniform Resource Locator 的简写,中文名叫统一资源定位符 用于表示服务端的各种资源,例如网页 下面将讲解 Flask 中如何提取组成 URL 的各个部分   URL 组 ...

  7. Flask上下文管理机制

    前引 在了解flask上下文管理机制之前,先来一波必知必会的知识点. 面向对象双下方法 首先,先来聊一聊面向对象中的一些特殊的双下划线方法,比如__call__.__getattr__系列.__get ...

  8. Flask-Login详解

    Flask-Login详解 关于Flask登录认证的详细过程请参见拙作<<使用Flask实现用户登陆认证的详细过程>>一文,而本文则偏重于详细介绍Flask-Login的原理, ...

  9. 6000字Locust入门详解

    目录 一.Locust 性能测试 (一). 性能测试工具 主流性能测试工具对比 认识Locust (二) locust 基本用法 1.安装locust 2.编写用例 3. 启动测试 GUI 模式启动 ...

随机推荐

  1. [JAVA]移位运算(左移<<,右移>>和无符号右移>>>)

    一.背景知识 整数在内存中是以二进制的形式存在的,而且存的是该整数的补码.最高位代表符号位,正数为0,负数为1 正数的补码是其二进制本身,负数的补码则是 符号位保持1不变,其他位按位取反再加1,+0和 ...

  2. v-model修饰符

    1).lazy:取代 input 监听 change 事件. 用法: v-model.lazy="message" 效果: 在input输入框改变数据时,v-model绑定的DOM ...

  3. 如何在Linux环境下用虚拟机跑Windows!

    文章目录 #0x0 Windows #0x1 安装虚拟机 #0x10 下载: #0x11 安装: #0x2 安装虚拟机windows #0x20 下载镜像 #0x21 安装镜像 #0x3 使用Wind ...

  4. 机器学习实战基础(十二):sklearn中的数据预处理和特征工程(五) 数据预处理 Preprocessing & Impute 之 处理分类特征:处理连续性特征 二值化与分段

    处理连续性特征 二值化与分段 sklearn.preprocessing.Binarizer根据阈值将数据二值化(将特征值设置为0或1),用于处理连续型变量.大于阈值的值映射为1,而小于或等于阈值的值 ...

  5. bzoj2132圈地计划

    bzoj2132圈地计划 题意: 一块土地可以纵横划分为N×M块小区域.于第i行第j列的区域,建造商业区将得到Aij收益,建造工业区将得到Bij收益.而如果区域(i,j)相邻(相邻是指两个格子有公共边 ...

  6. 【Python学习笔记一】基础环境安装:idea+python

    IDEA 安装 1.下载IDEA   官网下载地址: https://www.jetbrains.com/idea/ 2.安装的时候配置基本选择默认配置就行 参考链接:https://blog.csd ...

  7. 2.UDP协议

    UDP只在IP数据报服务之上增加了很少功能,即复用分用和差错检测功能. 应用层给UDP多长的报文,UDP就照样发送,即一次发送一个完整报文 一.UDP首部格式 这里的长度是指(首部+数据) UDP校验 ...

  8. C++语法小记---重载逻辑操作符

    重载逻辑操作符 不建议重载逻辑操作符 原因:无法实现逻辑操作符的短路功能(即:不需要计算完全部表达式就可以得出结果) 逻辑操作符:|| && 操作符重载本质上是函数调用,而进行函数调用 ...

  9. .Net、ASP.Net、C#、VisualStudio之间的关系是什么

    .Net一般指的是.NetFramework,提供了基础的.Net类,这些类可以被任何一种.Net编程语言调 用,.NetFramework还提供了 CLR.JIT.GC等基础功能. ASP.Net是 ...

  10. windows上Scrapy 框架的安装

    采用在命令行直接安装的方式:pip install scrapy报错:   到这个网站下载 Twisted:https://www.lfd.uci.edu/~gohlke/pythonlibs/进入网 ...