[置顶] Flask进阶(一)——请求上下文和应用上下文完全解答(上)

时间:2022-11-24 12:50:13

前言:

flask的轻便和强大的扩展性能会让web的初级开发者甚至是有经验的开发者神往。

flask能在短时间内快速搭建web的后台,而《flask web开发--基于python的web应用开发实战》是最好的flask入门教程了。

但当中对应用上下文和请求上下文的讲解有点简单,本文对这两个重要的概念做一个总结,方便自己以后的回顾。

由于不看源码是很难理解上下文的前世今生,所有本文在某些地方会涉及到flask的源码,由于flask和werkzug结合得很紧密,werkzeug也会有所展示。

预备知识:

1、Thread Local(本地线程)

从面向对象设计的角度看,对象是保存“状态”的地方。Python 也是如此,一个对象的状态都被保存在对象携带的一个特殊字典中。Thread Local 则是一种特殊的对象,它的“状态”对线程隔离 —— 也就是说每个线程对一个 Thread Local 对象的修改都不会影响其他线程。

(local.py)
import threading
mydata = threading.local()
mydata.number = 42
print mydata.number
log = []

def f():
mydata.number = 11
log.append(mydata.number)

thread = threading.Thread(target = f)
thread.start()
thread.join()
print log
print mydata.number

>python local.py
42
[11] #在线程内变成了mydata.number其他的值
42 #但是没有影响到开始设置的值
这种对象的实现原理也非常简单,只要以线程的 ID 来保存多份状态字典即可,就像按照门牌号隔开的一格一格的信箱。这样来说,只要能构造出 Thread Local 对象,就能够让同一个对象在多个线程下做到状态隔离。这个“线程”不一定要是系统线程,也可以是用户代码中的其他调度单元,例如 Greenlet。

2、Werkzeug的Local

flask和werkzeug结合紧密,werkzeug是flask的wsgi工具集,所以flask中使用的本地线程是werkzeug中实现的(werkzeug.local.Local)。

werkzeug.local.Local和threading.local区别如下:

(1)werkzeug使用了自定义的__storage__保存不同线程下的状态

(2)werkzeug提供了释放本地线程的release_local方法

(3)werkzeug通过get_ident函数来获得线程标识符

为什么werkzeug还自己搞了一套而不直接使用threading.local呢?
在python中,除了线程之外,还有个叫协程的东东,(这里不提进程)。java中貌似是无法实现协程的。而python的协程感觉高大尚的样子,python3.5开始对协程内置支持,而且也有相关开源库greenlet等。
协程是什么?
举个例子,比如一个线程在处理IO时,该线程是处于空闲状态的,等待IO返回。但是此时如果不让我们的线程干等着cpu时间片耗光,有没有其他办法,解决思路就是采用协程处理任务,一个线程中可以运行多个协程,当当前协程去处理IO时,线程可以马上调度其他协程继续运行,而不是干等着不干活。
这么一说,我们知道了协程会复用线程,WSGI不保证每个请求必须由一个线程来处理,如果WSGI服务器不是每个线程派发一个请求,而是每个协程派发一个请求,所以如果使用thread local变量可能会造成请求间数据相互干扰,因为一个线程中存在多个请求。
所以werkzeug给出了自己的解决方案:werkzeug.local模块。

除 Local 外,Werkzeug 还实现了两种数据结构:LocalStack 和 LocalProxy。

LocalStack 是用 Local 实现的栈结构,可以将对象推入、弹出,也可以快速拿到栈顶对象。当然,所有的修改都只在本线程可见。和 Local 一样,LocalStack 也同样实现了支持 release_pool 的接口。


LocalProxy 则是一个典型的代理模式实现,它在构造时接受一个 callable 的参数(比如一个函数),这个参数被调用后的返回值本身应该是一个 Thread Local 对象。对一个 LocalProxy 对象的所有操作,包括属性访问、方法调用(当然方法调用就是属性访问)甚至是二元操作 [6] 都会转发到那个 callable 参数返回的 Thread Local 对象上,因此也不会影响到其他线程。

3、flask上下文种类

application context request context
current_app(当前激活程序实例) request(请求对象,封装了http请求内容)
g(处理请求时临时存储的对象) session(用户会话,用于存储请求之间需要‘记住’的字典值)
这四种上下文的定义都在flask.globals.py中

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 _lookup_app_object(name):
top = _app_ctx_stack.top
if top is None:
raise RuntimeError(_app_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


# context locals
_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'))
g = LocalProxy(partial(_lookup_app_object, 'g'))

4、为什么要用上下文

flask从客户端获取到请求时,要让视图函数能访问一些对象,这样才能处理请求。例如请求对象就是一个很好的例子。要让视图函数访问请求对象,一个显而易见的方法就是将其作为参数传入视图函数,不过这回导致程序中的每个视图函数都增加一个参数,为了避免大量可有可无才参数把视图函数弄得一团糟,flask使用上下文临时把某些对象变为全局可访问(只是当前线程的全局可访问)。

正文:

1、请求上下文(request、session)

(1)生命周期

先回顾一个请求上下文的使用例子

@app.route('/')
def index():
user_agent = request.headers.get('User-Agent')
return'<p>Your broswer is %s<\p>' % user_agent

在视图函数中,可以直接使用request来获取请求对象。这里牵涉到请求上下文的生命周期问题了。

def handle_request():
print 'handle request'
print request.url

handle_request() #会收到运行时错误:RuntimeError: working outside of request context
request对象只有在请求上下文的生命周期内才可以访问。离开了请求的生命周期,其上下文环境也就不存在了,自然也无法获取request对象。

那什么是请求上下文的生命周期呢?一般就是在视图函数里,或者请求钩子中,才能使用请求上下文request。flask在接收到来自客户端的请求时,会帮我们构造请求上下文的使用环境,当flask根据url跳到相应的视图函数的时候,自然而然就可以直接使用请求上下文,请求钩子也是同理。

此外,在不同http请求之间得到的request必然也是不一样的,它只包含当前http请求的一些信息,是线程隔离的。

而session不一样,session是可以跨请求的,在不同的http请求之间,session都是同一个,因此,可以借session来存储一些请求之间需要‘记住’的字典值。

(2)请求上下文环境构造(本篇幅有点长,涉及flask在接收http请求后的工作过程)

from flask import Flask  

app = Flask(__name__) #生成app实例

@app.route('/')
def index():
return 'Hello World'

flask是遵循WSGI接口的web框架,因此它会实现一个类似如下形式的函数以供服务器调用:

def application(environ, start_response):               #一个符合wsgi协议的应用程序写法应该接受2个参数  
start_response('200 OK', [('Content-Type', 'text/html')]) #environ为http的相关信息,如请求头等 start_response则是响应信息
return [b'<h1>Hello, web!</h1>'] #return出来是响应内容
这个application在flask里叫做wsgi_app。服务器框架在接收到http请求的时候,去调用app时,他实际上是用了Flask 的 __call__方法,这点非常重要!!!
因为__call__方法怎么写,决定了你整个流程从哪里开始。

class Flask(_PackageBoundObject):        #Flask类  

#中间省略一些代码

def __call__(self, environ, start_response): #Flask实例的__call__方法
"""Shortcut for :attr:`wsgi_app`."""
return self.wsgi_app(environ, start_response) #注意他的return,他返回的时候,实际上是调用了wsgi_app这个功能
如此一来,我们便知道,当http请求从server发送过来的时候,他会启动__call__功能,最终实际是调用了wsgi_app功能并传入environ和start_response。

接下来查看flask中符合wsgi接口的这么一个函数wsgi_app:

class Flask(_PackageBoundObject):  

#中间省略一些代码
def wsgi_app(self, environ, start_response):
ctx = self.request_context(environ)
ctx.push()
error = None
try:
try:
response = self.full_dispatch_request() #full_dispatch_request起到了预处理和错误处理以及分发请求的作用
except Exception as e:
error = e
response = self.make_response(self.handle_exception(e)) #如果有错误发生,则生成错误响应
return response(environ, start_response) #如果没有错误发生,则正常响应请求,返回响应内容
finally:
if self.should_ignore_error(error):
error = None
ctx.auto_pop(error)
首先注意到这两行

       ctx = self.request_context(environ)  
ctx.push()
进去request_context方法看看:

class Flask(_PackageBoundObject): 
def request_context(self, environ):
return RequestContext(self, environ)
 
返回了一个RequestContext实例,于是查看类RequestContext的定义: 

class RequestContext(object):

def __init__(self, app, environ, request=None):
self.app = app #app是Flask(__name__)实例
if request is None:
request = app.request_class(environ) #request_class实质上是flask.wrappers下的class Request。这里根据environ创建一个Request实例
self.request = request
self.url_adapter = app.create_url_adapter(self.request)
self.flashes = None
self.session = None

self._implicit_app_ctx_stack = []
self.preserved = False
self._preserved_exc = None
self._after_request_functions = []

self.match_request()
第一个http请求过来时,request默认是空的,通过app.request_class(environ)构造

因此ctx获得一个刚初始化过的RequestContext实例,ctx包含了当前请求的request、session等各种信息,非常重要。

然后使用ctx.push()方法,这个方法很重要,继续查看:

class RequestContext(object):
def push(self):
top = _request_ctx_stack.top #top其实是个RequestContext实例
if top is not None and top.preserved:
top.pop(top._preserved_exc)
app_ctx = _app_ctx_stack.top
if app_ctx is None or app_ctx.app != self.app:
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()

_request_ctx_stack.push(self) #将自己入栈
self.session = self.app.open_session(self.request) #根据当前客户端的cookie和有效时间来获取保存好的会话
if self.session is None: #第一次http请求来时,得到的会话一般是空的
self.session = self.app.make_null_session() #创建一个新的会话
先从_request_ctx_stack获取栈顶元素。还记得flask.globals里的代码吗?

_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()

请求上下文栈和应用上下文栈都是LocalStack数据结构的实例

class LocalStack(object):

def __init__(self):
self._local = Local() #由此可以看出LocalStack实例包裹了一个Local类的实例
def push(self, obj):
rv = getattr(self._local, 'stack', None)
if rv is None:
self._local.stack = rv = []
rv.append(obj)
return rv

def pop(self):
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):
try:
return self._local.stack[-1]
except (AttributeError, IndexError):
return None
#LocalStack的push、pop、top的操作对象都是自身的_local对象。
因此,_request_ctx_stack和_app_ctx_stack的push、pop、top操作都是针对自己self._local.stack属性进行的。
而self._local是一个很重要属性,它是WerkZeug.local.Local的实例,是一个本地线程,因此不同线程之间的_request_ctx_stack和_app_ctx_stack是不一样的。
现在回到class RequestContext的push方法,看以下几行代码:
class RequestContext(object):
def push(self):
#省略了一些代码
_request_ctx_stack.push(self) #将自己入栈
self.session = self.app.open_session(self.request) #根据当前客户端的cookie和有效时间来获取保存好的会话
if self.session is None: #第一次http请求来时,得到的会话一般是空的
self.session = self.app.make_null_session() #创建一个新的会话
执行ctx.push之后会将ctx自己压入_request_ctx_stack栈中,那么创建的RequestContext实例就被保存到了_request_ctx_stack栈里面了。这里关注另一个问题,请求上下文中session。session最先是根据self.request来打开,这里会根据request的信息,比如当前http请求的cookie,来打开原来就保存好的,跟当前客户端对应的那个会话(不知道这样表述读者能不能理解)。因为要保证不同客户端过来时,打开的会话是不一样的。而同一个客户端的不同请求之间,打开的会话要是一样的。如果打开为空,则表面这个客户端是第一次链接,给它创建一个新的空的会话。
这时候,ctx已经被压栈,同时也已经携带好了request和session两个请求上下文了。但是还差一步,请求上下文的环境才算构造完成。
这时候需要回到globals.py里回看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)


# context locals
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()

request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))
request和session都是先从_request_ctx_stack里获取头元素,这个元素是RequestContext实例,它包含了request、session等等的很多信息。request和session就是从它身上获取的。这里可以看到,request和session其实是一个LocalProxy实例,LocalProxy其实就是一个代理,一个为werkzeug的Local对象服务的代理。他把所以作用到自己的操作全部“转发”到 它所代理的对象上去。它初始化的方法如下:

@implements_bool
class LocalProxy(object):

__slots__ = ('__local', '__dict__', '__name__', '__wrapped__')

def __init__(self, local, name=None):
# 这里有一个点需要注意一下,通过了__setattr__方法,self的
# "_LocalProxy__local" 属性被设置成了local,你可能会好奇
# 这个属性名称为什么这么奇怪,其实这是因为Python不支持真正的
# Private member,具体可以参见官方文档:
# http://docs.python.org/2/tutorial/classes.html#private-variables-and-class-local-references
# 在这里你只要把它当做 self.__local = 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)

LocalProxy初始化的时候接收一个可以调用的方法,并且设置为自身的_LocalProxy__local属性。因此request和session本质是一个LocalProxy实例,它们都有一个_LocalProxy__local属性,分别指向偏函数_lookup_req_object(request)和_lookup_req_object(session)。这时候请求上下文的环境就构造完成了。现在相信读者也应该理解了为什么这两个上下文只能在视图函数或者请求钩子里面使用了,因为只有这时候才具备了使用的环境。后面会介绍如何自行构造环境。

(3)请求上下文的使用

既然request和session本质是一个LocalProxy实例,那它是如何使用呢?这里看几个LocalProxy类下的重要的方法:

@implements_bool
class LocalProxy(object):

def _get_current_object(self):
"""
        获取当前被代理的真正对象,一般情况下不会主动调用这个方法,除非你因为
        某些性能原因需要获取做这个被代理的真正对象,或者你需要把它用来另外的
        地方。
        """
        # 这里主要是判断代理的对象是不是一个werkzeug的Local对象,在我们分析request
        # 的过程中,不会用到这块逻辑。
        if not hasattr(self.__local, '__release_local__'):
            # 从LocalProxy(partial(_lookup_req_object, 'request'))看来
            # 通过调用self.__local()方法,我们得到了 partial(_lookup_req_object, 'request')
            # 也就是 ``_request_ctx_stack.top.request``
            return self.__local()
        try:
            return getattr(self.__local, self.__name__)
        except AttributeError:
            raise RuntimeError('no object bound to %s' % self.__name__)
    # 接下来就是一大段一段的Python的魔法方法了,Local Proxy重载了(几乎)?所有Python
    # 内建魔法方法,让所有的关于他自己的operations都指向到了_get_current_object()
    # 所返回的对象,也就是真正的被代理对象。

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
request和session一般的使用如:request.form.get('args_name')、request.args.get('args_name') 或者session['name']=value。
这里分析一个简单的例子:page = request.args.get('page', 1, type=int)

这里会先调用request的__getattr__方法,返回getattr(self._get_current_object(),'args')获取真正的代理对象,即_request_ctx_stack栈顶的RequestContext元素,再在它身上得到request,再获取args。

class Flask(_PackageBoundObject):  
#中间省略一些代码
def wsgi_app(self, environ, start_response):
ctx = self.request_context(environ)
ctx.push()
error = None
try:
try:
response = self.full_dispatch_request() #full_dispatch_request起到了预处理和错误处理以及分发请求的作用
except Exception as e:
error = e
response = self.make_response(self.handle_exception(e)) #如果有错误发生,则生成错误响应
return response(environ, start_response) #如果没有错误发生,则正常响应请求,返回响应内容
finally:
if self.should_ignore_error(error):
error = None
ctx.auto_pop(error)
在视图函数处理完,wsgi生成完response之后,_request_ctx_stack会将栈顶这个RequestContext实例出栈。虽然ctx已经出栈,但是ctx.session已经通过序列化保存在本地了,但是request已经被抛弃。这就是为什么session可以跨请求使用了。

(4)自行构造请求上下文环境

可以仿照源码的形式,自行构造上下文:
from werkzeug.test import EnvironBuilder

ctx = app.request_context(EnvironBuilder('/','http://localhost/').get_environ())
ctx.push()
try:
print request.url
finally:
ctx.pop()
请求上下文基本就介绍完毕了。当然还有应用上下文,将在下篇介绍。
这里贴上几篇参考的博文,有兴趣的童鞋都可以看看: Flask进阶系列(一)–上下文环境 Flask 的 Context 机制 flask 上下文的实现 Werkzeug Local与LocalProxy等浅析
Python中的魔法方法深入理解
Flask源码解读 <1> --- 浅谈Flask基本工作流程
Flask源码解读 <2> --- 请求上下文和request对象