首页 > 其他分享 >flask-请求上下文分析

flask-请求上下文分析

时间:2023-04-06 20:36:34浏览次数:43  
标签:__ 请求 flask self request 线程 上下文 local def

1.请求上下文分析预备知识

1.1 导出项目依赖

我们之前使用导出项目依赖的命令是:

pip freeze > requirements.txt  # 导出项目依赖
pip install -r requirements.txt  # 安装项目依赖

这种方式更适合在虚拟环境的导出和导入,因为它会导出当前解释器所有的依赖。但是如果在本机的解释器环境,我们安装了很多依赖但是我们实际项目需要使用的依赖只是其中个别,那么我们需要按照以下命令更精确的导出依赖:
1.安装:

pip3 install pipreqs

2.生成项目依赖

pipreqs ./
#  windows电脑有可能会报错,如果报错执行:pipreqs ./ --encoding=utf8

1.2 函数和方法

函数和方法的区分:
函数:用类或者对象调用不能将自身传入,必须按照函数形参数量传值
方法:绑定给对象或者类的方法,由谁来调用可以将自身当做参数传入。面向对象中类中不加任何装饰器的是调用给对象的方法,加classmethod对的是方法,加staticmethod的是一个普通函数

类的绑定方法,对象也可以调用,会自动把产生对象的类当做参数传入:

class Student:
    @classmethod
    def stu(self):
        return self.__name__

stu1 = Student()
print(stu1.stu())  # Student

对象绑定的方法,类也可以调用,但是需要自己手动传参,就变成了普通的函数而不是方法,有几个值就要传几个值:

class Student:
    def stu(self):
        return self

print(Student.stu(1))  # 1

python中可以用isinstance判断对象是否是这个类产生的:

class Foo(object):
    def fetch(self):
        pass

    @classmethod
    def test(cls):
        pass

    @staticmethod
    def test1():
        pass

def add():
    pass

a = Foo()
print(isinstance(a,Foo))  # True
print(isinstance('a',Foo))  # False
'''也可以用来判断数据类型,数据类型也是一个类,产生的数据都是该类的对象'''
print(isinstance(1,int))  # True

issubclass:判断子类是否继承自该父类:

class Student:
    pass

class Stu1(Student):
    pass

print(issubclass(Stu1,Student))  # True

了解了issubclass和isinstance,下面就可以来判断函数混合方法:
在python中有两个关键字,是判断他们是方法还是函数:MethodType:方法,FunctionType:函数

#  导入
from types import MethodType, FunctionType

class Foo(object):
    def fetch(self):
        pass

    @classmethod
    def test(cls):
        pass

    @staticmethod
    def test1():
        pass

def add():
    pass
	
print(isinstance(Foo.fetch, MethodType))  # False  类调用对象的方法,就是一个普通函数
print(isinstance(Foo.fetch, FunctionType))  # True

print(isinstance(obj.fetch,MethodType))  # True  绑定给对象的方法,对象来调用,是一个方法
print(isinstance(obj.fetch,FunctionType))  # False

print(isinstance(add,FunctionType))  # True

print(isinstance(Foo.test,MethodType))  # True  类调用绑定给自身的方法,是一个方法
print(isinstance(Foo.test,FunctionType))  #False

print(isinstance(obj.test,MethodType))  # True  对象调用绑定给类的方法,将父类当做参数传入,是一个方法
print(isinstance(obj.test,FunctionType))  # False

print(isinstance(obj.test1,FunctionType))  # True  静态方法是一个普通函数,不管是对象调用还是类调用
print(isinstance(Foo.test1,FunctionType))  # True

2.threading.local对象

并发编程时,多个线程操作同一个变量,会出现并发安全的问题,需要加锁。而是用local对象多线程并发操作时,不需要加锁,不会出现数据错乱。
本质原理:多线程修改同一个数据,复制多份变量,为每个线程开辟一块空间进行数据存储,每个线程操作自己的那部分数据。

在全局定义一个a,开启10个线程依次修改全局的a,因为一个进程下只有一个线程在执行,所以会依次修改a,从0到9,这也验证了gil锁的存在:

from threading import Thread, get_ident

a = 0

def task(args):
    global a
    a = args
    print('线程id为:%s' % get_ident(), a)

for i in range(10):
    t = Thread(target=task, args=(i,))
    t.start()

image
当我们在task内部加上time.sleep之后,得到的结果全部是9。这是因为当第一次修改之后程序进入阻塞态,cpu去执行下一次循环修改a,直到最后一次修改a为9。当所有程序到就绪态打印的a都是全局的a:

from threading import Thread, get_ident
import time
a = 0

def task(args):
    global a
    a = args
    time.sleep(2)
    print('线程id为:%s' % get_ident(), a)

for i in range(10):
    t = Thread(target=task, args=(i,))
    t.start()
'''打印的结果全部为9'''

如果我们想解决数据错乱,达到刚才的效果(a分别打印0到9),有三种方案:
方法1:用join()控制每一个线程执行完毕之后,再释放cpu:

from threading import Thread, get_ident
import time

a = 0

def task(args):
    global a
    a = args
    time.sleep(2)
    print('线程id为:%s' % get_ident(), a)

for i in range(10):
    t = Thread(target=task, args=(i,))
    t.start()
    t.join()

image
方式二:用threading模块中自带的互斥锁,执行完毕之后再释放锁(用互斥锁解决并发安全的问题):

from threading import Thread, get_ident,Lock
import time
lock = Lock()

a = 0

def task(args):
    lock.acquire()
    global a
    a = args
    time.sleep(2)
    print('线程id为:%s' % get_ident(), a)
    lock.release()

for i in range(10):
    t = Thread(target=task, args=(i,))
    t.start()

image
只需要在数据可能发生错乱的地方加锁串行,其他地方可以并行
方式三:使用local对象,避免并发安全的问题:
虽然修改的都是全局对象对象a的args属性,但是每个对象修改的都是自己那个部分,并不会发生数据错乱

from threading import Thread, get_ident,local
import time
#  生成一个local对象
a = local()

def task(args):
#  给local对象添加属性
    a.args = args
    time.sleep(2)
#  只能打印a.args,如果直接打印a的话结果是对象a的内存地址
    print('线程id为:%s' % get_ident(), a.args)

for i in range(10):
    t = Thread(target=task, args=(i,))
    t.start()

image
这和flask中的request类似,虽然我们每次请求用的都是全局的request,但是每次请求的request对象的属性都只是本次请求线程的数据,并不会和其他请求线程的数据发生混淆和错乱。这是因为flask中的request就是用了local,但是它只能区分线程,不能区分协程。

协程:单线程下的并发。
python中的多线程实际上是由一个线程在程序之间来回切换产生的。而协程是程序层面的切换,程序层面切换耗费资源较少。也就是协程还是由同一个线程来回切换产生的并发效果,cpu(在线程上)遇到io操作就去执行下一个程序,实现单线程下的并发效果。既然是同一个线程,使用loacl必然会导致数据错乱。

local本质使用线程来区分数据,也就是每个线程上都有自己独立的数据不受其他线程的影响:

{'线程id号':{args:1},{'线程id号':{args,2}}...}

但是如果是使用协程,local本质依然可以按照协程来区分数据,本质如下:

{'协程id号':{args:1},{'协程id号':{args,2}}...}

3.兼容线程和协程的local对象

正是因为gil锁的存在,在一个进程下开多个线程,也只能有一个线程在执行。如果我们想要利用多核优势,就需要开多个进程(比如是4核,那么就需要开4个进程)。这也就是计算密集型程序要用多进程的原因,因为这样才能充分利用多核优势。于是在线程下开携程,一个进程下只能有一个线程在运行,所以我们要利用协程来充分榨干cpu。
接下来用local对象封装一个兼容线程和协程的local对象:

try:
    '''如果能导入说明使用协程访问'''
    from greenlet import getcurrent as get_ident
except Exception as e:
    '''否则是用线程访问的'''
    from threading import get_ident

from threading import Thread
import time


class Local(object):
    def __init__(self):
        '''用类object点方法而不是用对象,是因为对象点那么self就是Local的对象。对象.storage=值会触发
        __setattr__方法,所以如果是对象点结果就是执行__setattr__方法的前四行,产生递归深度的问题'''
        object.__setattr__(self, 'storage', {})

    def __setattr__(self, k, v):
        ident = get_ident()
#  这里的get_ident()方法如果是线程访问的产生的结果就是线程,否则就是协程
        if ident in self.storage:
            '''storage:{线程id:{k:v},协程id:{k"v}...}'''
            self.storage[ident][k] = v
        else:
            self.storage = {k: v}

    def __getattr__(self, k):
        ident = get_ident()
        return self.storage[ident][k]


obj = Local()
def task(arg):
    obj.val = arg
    obj.xxx = arg
    print(obj.val)

for i in range(10):
    t = Thread(target=task,args=(i,))
    t.start()

4.偏函数

当我们只知道函数参数中的部分时,我们可以分批次给函数传参:
关键字传参:

from functools import partial

def index(a, b, c):
    return a * b * c

# print(index(2,3,4))  # 24

index = partial(index,2,3)
print(index(4)) #  24

也可以利用位置传参:

from functools import partial

def index(a, b, c):
    return a * b * c

# print(index(2,3,4))  # 24

index = partial(index,b=2,c=3)
print(index(a=4))

5.flask执行流程(以1.1.4版本为例)

注:1.1.4版本的Flask要搭配2.0.1版本的MarkupSafe
1.我们手写一个简单的flask框架:

from flask import Flask

app = Flask(__name__)

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

if __name__ == '__main__':
    app.run()

2.当请求来的时候,执行了run()方法,实际是在执行run()方法中的run_simple()方法:
run方法:

def run():
	try:
		run_simple(host, port, self, **options)
	finally:
		self._got_first_request = False

3.run_simple有一种特殊的机制:
run_simple('localhost',4000,hello)执行的结果是hello()。而
run_simple(host, port, self, **options)中的self就是Flask的对象app,run_simple(host, port, self, **options)的结果也就是app()。app()会触发Flask中的__call__。
flask/call.py:

    def __call__(self, environ, start_response):
        return self.wsgi_app(environ, start_response)

本质是在执行wsgi_app(environ, start_response)
4.wsgi_app方法其实就是整个flask请求进来到处理请求到返回结果的整个生命周期的内容都在这个函数中处理:
4.1它包装了request
4.2执行了请求扩展
4.3根据路由匹配视图函数
4.4执行视图函数
4.5视图函数返回结果

    def wsgi_app(self, environ, start_response):
	'''environ是当次http请求的字典对象'''
        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)

5.上述代码中self是Flask对象,也就是app,self点request_context方法,下面看request_context方法:

    def request_context(self, environ):
        return RequestContext(self, environ)

request_context执行的结果是RequestContext(请求上下文)对象
6.RequestContext源码:

class RequestContext(object):
    def __init__(self, app, environ, request=None, session=None):
        self.app = app
'''刚开始request是None'''
        if request is None:
            request = app.request_class(environ)
"""
点进request_class源码:
request_class = Request
所以request也就是Request的一个对象,把当次请求字典传入,request就拥有了当次请求的属性
"""
        self.request = request
'''将request赋值给当次请求的Flask对象self'''
		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

7.再回到wsgi_app方法:

'''ctx是类RequestContext的对象,当次请求的信息都在ctx中'''
ctx = self.request_context(environ)
        error = None
        try:
            try:
                ctx.push()
                response = self.full_dispatch_request()

ctx.push():现在去类RequestContext对象查找方法push()
8.RequestContext下push

def push()
	_request_ctx_stack.push(self)
'''此时self是RequestContext的对象ctx'''
        if self.session is None:
            session_interface = self.app.session_interface
            self.session = session_interface.open_session(self.app, self.request)
'''如果session是空的,那么执行方法open_session'''

9.点进_request_ctx_stack方法:

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

所以_request_ctx_stack.push(self)就相当于LocalStack().push(self),此时的self是ctx对象
10.LocalStack中push方法:

    def push(self, obj):
        """Pushes a new item to the stack"""
        rv = getattr(self._local, "stack", None)
'''
self是类LocalStack的对象,去__init__中找_local方法,发现_local是Local的一个对象,这个Local()对象可以区分线程或者协程(类似于我们刚才自己定义的)
    def __init__(self):
        self._local = Local()
'''
        if rv is None:
            self._local.stack = rv = []
        rv.append(obj)
'''此时的rv是:
{线程id号1:{stack:[]},线程id号2:{stack:[]}...}
'''
        return rv

11.当请求走到视图函数中,此时的request并不是真的request。点进request源码:

request = LocalProxy(partial(_lookup_req_object, "request"))

发现request其实是类LocalProxy的对象。当我们执行print(request)是会触发类LocalProxy的__str__方法。request.method会触发类LocalProxy的__getattr__方法。现在去类LocalProxy中找__getattr__方法。
12.LocalProxy下__getattr__方法:

    def __getattr__(self, name):
...
        return getattr(self._get_current_object(), name)

LocalProxy下_get_current_object()方法

    def _get_current_object(self):
        if not hasattr(self.__local, "__release_local__"):
'''__local()是Local对象,有值'''
'''__local()是Local对象,有值'''
#self.__local()  在init的时候,实例化的,在init中:object.__setattr__(self, "_LocalProxy__local", local)
# 用了隐藏属性
#self.__local 实例化该类的时候传入的local(偏函数的内存地址:partial(_lookup_req_object, "request"))
#加括号返回,就会执行偏函数,也就是执行_lookup_req_object,不需要传参数了
#这个地方的返回值就是request对象(当此请求的request,没有乱)
            return self.__local()
        try:
            return getattr(self.__local, self.__name__)
        except AttributeError:
            raise RuntimeError("no object bound to %s" % self.__name__)

综上所述:每次请求的request都是自己的,session、g对象同理。

标签:__,请求,flask,self,request,线程,上下文,local,def
From: https://www.cnblogs.com/ERROR404Notfound/p/17293085.html

相关文章

  • 请求上下文分析、函数和方法、threading.local对象、偏函数、flask整个生命执行流程(1
    请求上下文分析(源码:request原理)导出项目的依赖#之前pipfreeze>requirments.txt把当前解释器环境下的所有第三方依赖都导出来#使用第三方模块,更精确的导出依赖pipreqs第一步:安装pip3installpipreqs第二步:使用命令,导出项目依赖pipreqs./w......
  • 【flask】flask请求上下文分析 threading.local对象 偏函数 flask1.1.4生命执行流程
    目录上节回顾今日内容1请求上下文分析(源码:request原理)1.1导出项目的依赖1.2函数和方法1.3threading.local对象1.4偏函数1.5flask整个生命执行流程(1.1.4版本为例)2wtforms(了解)补充上节回顾#1蓝图 -第一步:导入-第二步:实例化得到对象,可以指定static和templates......
  • flask之request源码和第三方模块wtforms
    目录请求上下文分析(源码:request原理)1.导出项目的依赖2.函数和方法3.threading下的local对象4.偏函数5.flask整个生命执行流程---flask1.1.41版本为例wtforms---了解请求上下文分析(源码:request原理)1.导出项目的依赖以前导出项目的依赖:pipfreeze>requirements.txt......
  • MySQL Others--优化autocommit会话参数设置请求
    问题描述在排查QPS较高的MySQL集群过程中发现,部分MySQL集群约50%的请求为"SETautocommit=1",每次业务请求前都会执行1次"SETautocommit=1"操作,虽然单次”SETautocommit=1“操作并不会消耗过多MySQL服务器资源,但高QPS场景下频繁执行"SETautocommit=1"操作,严重浪费应用服务器和M......
  • Springboot+HTML5+Layui2.7.6上传文件【请求上传接口出现异常】
    1.最近两天在springboot+html5项目中发现在用layui框架时报请求上传接口出现异常这个错误。2.将代码全部整理了一遍,发现前端后台都没错!!!但是还是【请求上传接口出现异常】,于是跑去翻看layui官网。 3.最终最终将错误锁定到了返回的JSON字符串中,我是返回的String,所以一直都会......
  • flask源码分析
    目录请求上下文分析(源码:request原理)导出项目的依赖函数和方法threading.local对象偏函数flask整个生命执行流程(1.1.4版本为例)wtforms请求上下文分析(源码:request原理)导出项目的依赖之前的pipfreeze>requeirments.txt会把当前解释器环境下的所有第三方依赖都导出来......
  • flask4
    今日内容1请求上下文分析(源码:request原理)1.1导出项目的依赖#之前pipfreeze>requirments.txt把当前解释器环境下的所有第三方依赖都导出来#使用第三方模块,更精确的导出依赖pipreqs 第一步:安装pip3installpipreqs第二步:使用命令,导出项目依赖pipreqs./......
  • html 元素定位与接口请求总结
    1.下拉框环境:测试生产 <selectid="sid"onchange=""style="margin-right:20px;width:100px;"><optionid="dev"value="dev">测试</option><optionid="prod"value="prod"......
  • 1 请求上下文分析(源码:request原理)、2 wtforms(了解)
    目录1请求上下文分析(源码:request原理)1.1导出项目的依赖1.2函数和方法1.3threading.local对象1.4偏函数1.5flask整个生命执行流程(1.1.4版本为例)2wtforms(了解)1请求上下文分析(源码:request原理)1.1导出项目的依赖#之前pipfreeze>requirments.txt把当前解释器环境下......
  • flask:蓝图(blueprint)、g对象、数据库连接池
    目录一、蓝图(blueprint)1、蓝图介绍2、蓝图的使用3、使用蓝图,划分小型项目目录4、使用蓝图,划分大型项目目录5、其他知识点二、g对象三、数据库连接池一、蓝图(blueprint)1、蓝图介绍在Flask中,使用蓝图Blueprint来分模块组织管理。蓝图实际可以理解为是一个存储一组视图方法的容器......