源码解读:Flask上下文与代理模式

在上一节中,我跟大家一起深入了解了一下Python的「上下文管理器 」。而今天呢,还是和上下文有关的话题。只不过这里的上下文和上一节的内容有点不一样,上下文管理器是管理代码块级别的上下文,而今天要讲的上下文是工程项目中的上下文。

可能你现在对上下文这个概念,还是不太清楚。这里再简单说明一下

一段程序或函数的运行,很多情况下,是需要依赖程序外的变量才能够运行的,一旦脱离了这些变量,程序就没法正常工作。这些外部变量,按照常规的做法,是将这些变量做为函数的参数,一个一个地传入进去。这是一种做法,对于简单的小程序完全没有问题,一旦在较大的工程项目中,还采用这样的做法,就显得过于笨重,不灵活了。

一个较好的做法是,将这些项目全局中需要频繁用到的变量值整合在一起,而这些值的集合就是上下文。只要在需要的时候,从这个上下文中取就好了。

在 Flask 中,有两种上下文

  • application context
  • request context

application context 会存储一个 app 里可以全局共享里的变量。而 request context 会存储一个从外部发起的请求的所有信息。

一个 Flask 项目里,可以有多个 app, 而在一个 app 里会有多个 request。这是它们之间的对应关系。

上面说了,上下文可以实现 信息共享,但同时有一点很重要,就是 信息隔离 。在多个 app 同时运行的时候,要能保证一个 app 不能访问和改变到另一个 app 的变量。这个很重要。

那么具体是如何做到 信息隔离 的呢?

接下来,就要提到在 Flask 中三个很常见的对象,你应该不会感到陌生。

  • Local
  • LocalProxy
  • LocalStack

这三个对象都是 werkzeug 里提供的,定义的 local.py 里,所以它们并不是Flask 中特有的, 这就意味着我们可以直接在自己的项目中使用它们,而不用依托于 Flask 的环境。

3.5.1 Local

首先是 Local ,记得在以前的「并发编程系列」的第五篇线程的 信息隔离 的时候,提过了 threading.local ,它是专门用来存储当前线程的变量,从而实现对象的线程隔离。

而 Flask 里的 Local 和这它的是一个作用。

为了搞懂这个 Local 的作用,最好的方式就是直接对比两段代码。

首先是,不使用 Local 的情况,我们新建一个类,里面有name属性,默认为 wangbm,然后开始一个线程,做的事就是将这个 name 属性改为 wuyanzu。

import time
import threading class People:
name = 'wangbm' my_obj = People() def worker():
my_obj.name = 'wuyanzu' new_task = threading.Thread(target=worker)
new_task.start() # 休眠1s,保证子线程先执行
time.sleep(1) print(my_obj.name)

执行结果可想而知,是 wuyanzu 。子线程 对对象的更改可以直接影响到主线程。

接下来,我们使用 Local 来实现一下

import time
import threading from werkzeug.local import Local class People:
name = 'wangbm' my_obj = Local()
my_obj.name = 'wangbm' def worker():
my_obj.name = 'wuyanzu'
print('in subprocess, my_obj.name: '+str(my_obj.name)) new_task = threading.Thread(target=worker)
new_task.start() # 休眠1s,保证子线程先执行
time.sleep(1) print('in mainprocess, my_obj.name: '+str(my_obj.name))

打印结果如下,可见子线程的修改并不会影响主线程

in subprocess, my_obj.name: wuyanzu
in mainprocess, my_obj.name: wangbm

那么 Local 是如何做到的呢,其实原理很简单,就是利用了基本的数据结构:字典。

当线程去修改 Local 对象里的变量(包含变量名 k1 和变量值 v1 )时,通过源码可知,他是先获取当前线程的id,作为__storage__ (这个storage是个嵌套字典)的key,而value 呢,就是一个字典,{k1: v1}

def __setattr__(self, name, value):
ident = self.__ident_func__()
storage = self.__storage__
try:
storage[ident][name] = value
except KeyError:
storage[ident] = {name: value}

举例如下

# 0 和 1 是线程 id
self.__storage__['0'][k1] = v1
self.__storage__['1'][k2] = v2

正时因为用了线程id 作了一层封装,才得以实现了线程隔离。

如果要用图来表示,最开始的Local对象就是一个空盒子

当有不同的线程往里写数据时,Local 对象为每个线程分配了一个 micro-box。

local 是需要被 localmanager 管理的,在请求结束后,会调用 localmanager.cleanup() 函数,其实是调用 local.__release_local__ 进行数据清理。是如何做到的呢,看下面这段代码。

from werkzeug.local import Local, LocalManager

local = Local()
local_manager = LocalManager([local]) def application(environ, start_response):
local.request = request = Request(environ)
... # make_middleware会确保当request结束时,所有存储于local中的对象的reference被清除
application = local_manager.make_middleware(application)

以下就是 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)

3.5.2 LocalStack

通过对 Local 的介绍,可以知道 Local 其实是通过封装了字典的,以此实现了线程隔离。

而接下来要介绍的 LocalStack ,也是同样的思想,LocalStack 是封装了 Local ,所以它既有了 Local 的线程隔离的特性,也有了栈结构的特性,可以通过 pop,push,top 来访问对象。

同样用一张图来表示

栈结构的特性,无非就是后进先出。这里就不说了,这里的重点是线程隔离的特性如何体现,还是以上面的例子,稍微做了下修改。

import time
import threading from werkzeug.local import LocalStack my_stack = LocalStack()
my_stack.push('wangbm') def worker():
print('in subthread, my_stack.top is : '+str(my_stack.top) + ' before push')
my_stack.push('wuyanzu')
print('in subthread, my_stack.top is : ' + str(my_stack.top) + ' after push') new_task = threading.Thread(target=worker)
new_task.start() # 休眠1s,保证子线程先执行
time.sleep(1) print('in main thread, my_stack.top is : '+str(my_stack.top))

输出的结果如下,可见子线程的里的 my_stack 和主线程里的 my_stack 并不能共享,确实实现了隔离。

in subthread, my_stack.top is : None before push
in subthread, my_stack.top is : wuyanzu after push
in main thread, my_stack.top is : wangbm

在 Flask 中,主要有两种上下文,AppContext 和 RequestContext

当一个请求发起后,Flask 会先开启一个线程,然后将包含请求信息的上下文 RequestContext 推入一个 LocalStack 对象中(_request_ctx_stack),而在推入之前,其实它会去检测另一个 LocalStack对象(_app_ctx_stack)是否为空(但是一般 _app_ctx_stack 都不会为空),如果为空就先将app的上下文信息push到_app_ctx_stack,然后再去把请求的上下文信息push到_request_ctx_stack 里。

在flask中有三个对象比较常用

  • current_app
  • request
  • session

这三个对象,永远是指向LocalStack 栈顶的上下文中对应的app、request或者session,对应的源码如下:

def _lookup_req_object(name):
top = _request_ctx_stack.top
if top is None:
raise RuntimeError(_request_ctx_err_msg)
return getattr(top, name) def _find_app():
top = _app_ctx_stack.top
if top is None:
raise RuntimeError(_app_ctx_err_msg)
return top.app _request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))

3.5.3 LocalProxy

通过上面的代码,你可以发现,我们访问LocalStack里的元素的时候,都是通过LocalProxy 来进行的有没有?

这就很奇怪了,为什么不直接访问Local 和 LocalStack呢?

这应该是个难点,我这边举个例子,也许你就明白了。

首先是不使用LocalProxy的情况

# use Local object directly
from werkzeug.local import LocalStack user_stack = LocalStack()
user_stack.push({'name': 'Bob'})
user_stack.push({'name': 'John'}) def get_user():
# do something to get User object and return it
return user_stack.pop() # 直接调用函数获取user对象
user = get_user()
print user['name']
print user['name']

输出结果是

John
John

使用LocalProxy后

# use LocalProxy
from werkzeug.local import LocalStack, LocalProxy
user_stack = LocalStack()
user_stack.push({'name': 'Bob'})
user_stack.push({'name': 'John'}) def get_user():
# do something to get User object and return it
return user_stack.pop() # 通过LocalProxy使用user对象
user = LocalProxy(get_user)
print user['name']
print user['name']

输出结果

John
Bob

怎么样,看出区别了吧,直接使用LocalStack对象,user一旦赋值就无法再动态更新了,而使用Proxy,每次调用操作符(这里[]操作符用于获取属性),都会重新获取user, 从而实现了动态更新user的效果

每次 user['name'] 的时候 就会触发 LocalProxy 类的 __getitem__,从而调用该类的 _get_current_object。而每次 _get_current_object都会返回 get_user()(在flask中对应的函数是 _lookup_req_object ) 的执行结果, 也就是 user_stack.pop()

def __init__(self, local, name=None):
# 【重要】将local对象(也就是一个get_user函数对象)赋值给self.__local
object.__setattr__(self, '_LocalProxy__local', local)
object.__setattr__(self, '__name__', name)
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) def _get_current_object(self):
"""Return the current object. This is useful if you want the real
object behind the proxy at a time for performance reasons or because
you want to pass the object into a different context.
"""
if not hasattr(self.__local, '__release_local__'):
# 【重要】执行传递进行的 get_user 对象。
return self.__local()
try:
return getattr(self.__local, self.__name__)
except AttributeError:
raise RuntimeError('no object bound to %s' % self.__name__)

这样就能实现每次对栈顶元素的操作,都是面对最新元素执行的。

3.5.4 经典错误

在 Flask 中经常会遇到的一个错误是:

Working outside of application context.

这个错误,如果没有理解 flask 的上下文机制,是很难理解的。通过上面知识背景的铺垫,我们可以尝试来搞懂一下为什么会出现这样的情况。

首先我们先来模拟一下这个错误的产生。假设现在有一个单独的文件,内容如下

from flask import current_app

app = Flask(__name__)

app = current_app
print(app.config['DEBUG'])

运行一下,会报如下错误。

Traceback (most recent call last):
File "/Users/MING/PycharmProjects/fisher/app/mytest/mytest.py", line 19, in <module>
print(app.config['DEBUG'])
File "/Users/MING/.virtualenvs/fisher-gSdA58aK/lib/python3.6/site-packages/werkzeug/local.py", line 347, in __getattr__
return getattr(self._get_current_object(), name)
File "/Users/MING/.virtualenvs/fisher-gSdA58aK/lib/python3.6/site-packages/werkzeug/local.py", line 306, in _get_current_object
return self.__local()
File "/Users/MING/.virtualenvs/fisher-gSdA58aK/lib/python3.6/site-packages/flask/globals.py", line 51, in _find_app
raise RuntimeError(_app_ctx_err_msg)
RuntimeError: Working outside of application context.

你一定会奇怪吧。我明明也实例化一个app对象,但是为什么取current_app会报错呢?而如果不用current_app,就不会报错。

如果你认真学习了上面的内容,这边也就不难理解了。

从先前的研究发现,当使用current_app时,它取的是LocalStack的栈顶元素(app的上下文信息),而实际上在我们通过app = Flask(__name__)实例化一个app对象时,此时还没有将这个上下文信息写入LocalStack,自然取栈顶元素就会出错了。

def _find_app():
top = _app_ctx_stack.top
if top is None:
raise RuntimeError(_app_ctx_err_msg)
return top.app

上面我们也说过了,这个上下文什么时候push进去呢?在外部发起一起request请求后,首先就会先检查 app 的上下文信息是否已经 push 进去了,如果没有的话,就会先半其push进去。

而上面我们是以运行单个文件的方式,并没有实际产生一个 request 请求,自然 在 LocalStack 里没有 app的上下文信息。报错也是正常的。

知道了错误根源后,如何解决这种问题呢?

在Flask中,它提供了一个方法ctx=app.app_context()可以获取一个上下文对象,我们只要将这个上下文对象 手动 push 到 LocalStack 中,current_app 也就可以正常取到我们的app对象了。

from flask import Flask, current_app

app = Flask(__name__)
ctx = app.app_context()
ctx.push() app = current_app
print(app.config['DEBUG'])
ctx.pop()

由于 AppContext 类实现了上下文协议

class AppContext(object):
def __enter__(self):
self.push()
return self def __exit__(self, exc_type, exc_value, tb):
self.pop(exc_value) if BROKEN_PYPY_CTXMGR_EXIT and exc_type is not None:
reraise(exc_type, exc_value, tb)

所以你也可以这样写

from flask import Flask, current_app

app = Flask(__name__)

with app.app_context():
app = current_app
print(app.config['DEBUG'])

以上,是我通过学习七月的 Flask高级编程 加上自己直白的理解,希望对你在理解 Flask的上下文核心机制 会有帮助。

http://python-online.cn/zh_CN/latest/c03/c03_05.html#

fontDialog-字体对话框和colorDialog-颜色对话框的更多相关文章

  1. VS2010中新控件的编程------颜色按钮类和颜色对话框

    (1)      颜色按钮类和颜色对话框 1) 颜色对话框 MFC提供了颜色对话框类CMFCColorDialog进行颜色的选择,系统可以利用DoModal()调用,然后选择相应的颜色. CMFCCo ...

  2. 19.QT对话框(文件对话框,颜色对话框,字体框,自定义对话框)

    文件对话框 #include<QFileDialog> //文件对话框 void Dialog::on_pushButton_clicked() { //定义显示文件的类型 窗口标题 可供 ...

  3. QDialog:输入对话框、颜色对话框、字体对话框、文件对话框

    # _*_ coding:utf-8 _*_ import sys from PyQt4 import QtCore,QtGui class Example(QtGui.QWidget): def _ ...

  4. MFC编程入门之十九(对话框:颜色对话框)

    在上一节中为大家讲解了字体对话框的使用方法,熟悉了字体对话框,本节继续讲另一种通用对话框--颜色对话框. 颜色对话框大家肯定也不陌生,我们可以打开它选择需要的颜色,简单说,它的作用是用来选择颜色.MF ...

  5. VS2010/MFC对话框:颜色对话框

    颜色对话框 在上一节中为大家讲解了字体对话框的使用方法,熟悉了字体对话框,本节继续讲另一种通用对话框--颜色对话框. 颜色对话框大家肯定也不陌生,我们可以打开它选择需要的颜色,简单说,它的作用就是用来 ...

  6. VC编程中如何设置对话框的背景颜色和静态文本颜色

    晚上编一个小程序,涉及到如何设置对话框的背景颜色和静态文本颜色.这在VC6.0中本来是一句话就搞定的事.在应用程序类中的InitInstance()函数添加: //设置对话框背景和文本颜色 SetDi ...

  7. PyQt4颜色对话框QColorDialog

    QColorDialog提供了用于显示颜色的对话框. #!/usr/bin/python # -*- coding: utf-8 -*- import sys from PyQt4 import Qt ...

  8. VS2010/MFC编程入门之十九(对话框:颜色对话框)

    鸡啄米在上一节中为大家讲解了字体对话框的使用方法,熟悉了字体对话框,本节继续讲另一种通用对话框--颜色对话框. 颜色对话框大家肯定也不陌生,我们可以打开它选择需要的颜色,简单说,它的作用就是用来选择颜 ...

  9. c#打开颜色对话框

    用button时间 调出颜色对话框来: private void btnForeColor_Click(object sender, EventArgs e)        {            ...

  10. 【VS开发】VS2010 MFC中控件、对话框等背景颜色动态修改的方法

    [VS开发]VS2010 MFC中控件.对话框等背景颜色动态修改的方法 标签(空格分隔):[VS开发] 声明:引用请注明出处http://blog.csdn.net/lg1259156776/ 说明: ...

随机推荐

  1. android4.0 USB Camera实例(三)UVC

    前面我写了两篇文章说明了zc301的实现 详细请看 http://blog.csdn.net/hclydao/article/details/21235919 以下顺便把通用的USB也写上 前面的ZC ...

  2. 用rpm安装软件的常用步骤

    假设软件叫software.rpm 1.安装前,查看是否安装过用 rpm -q software 2.安装时,用 rpm -ivh software.rpm 3.安装后想删除,用 rpm -e sof ...

  3. JavaScript 正则表达式——基本语法(2)

    来源:http://www.cnblogs.com/dolphinX/p/3486214.html 定义 JavaScript种正则表达式有两种定义方式,定义一个匹配类似 <%XXX%>  ...

  4. Flutter混合栈的管理

    Flutter出现的目的旨在统一Android/IOS两端编程,因此完全基于Flutter开发的App,只需提供一个包含FlutterView的页面,后续页面增加/删除/跳转均在FlutterView ...

  5. python发送邮件实例1

    文件形式的邮件 #!/usr/bin/env python3 #coding: utf-8 import smtplib from email.mime.text import MIMEText fr ...

  6. Emitting signals

    Objects created from a QtCore.QObject can emit signals. In the following example we will see how we ...

  7. 开发基于CXF的 RESTful WebService web 项目 webservice发布

    配置步骤 开发基于CXF的 RESTful WebService 1.创建Web项目并导入CXF的jar 2.在Web.xml中配置 CXFServlet <servlet> <se ...

  8. web普通项目映射为maven项目

    localhost:8080/yourWebLocation  访问 前一个插件是映射项目路径成maven的格式 后一个插件是映射访问网址所需要的东西 <build> <!-- 发布 ...

  9. NTP服务及时间同步(CentOS6.x)(转)

    今有一小型项目,完全自主弄,原来以为很简单的NTP服务,我给折腾了2个多小时才整撑头(以前都是运维搞,没太注意,所以这技术的东西,在简单都需要亲尝啊),这里记录为以后别再浪费时间. 目标环境,5台li ...

  10. 微信公众平台自定义菜单及高级接口PHP SDK(转)

    本文介绍介绍微信公众平台自定义菜单及高级接口的PHP SDK及使用方法. 作者 方倍工作室 修正记录: 2014.05.03 v1.0 方倍工作室 http://www.cnblogs.com/txw ...