Web后端学习笔记 Flask(11)Local线程隔离对象

flask中的上下文:应用上下文和请求上下文

1. 在flask中,是通过request对象获取用户提交的数据,但是在整个程序运行中,只有一个request对象。在实际应用场景中,会有多个用户同时进行数据提交。此时应该开多个子线程,或者协程进行处理(即有多个request独立对象)。在Flask中通过Local解决这一问题。

只要绑定在Local对象上的属性,在每个线程中都是隔离的

Web后端学习笔记 Flask(11)Local线程隔离对象_第1张图片

local对象的原理:在local对象中,有一个字典,字典中存储的是,线程的id, 以及用户的请求内容。在多线程中,如果需要访问request中的值,因为此时的request对象是绑定在local对象上的,因此local对象会根据当前代码在哪一个线程下运行的,找到线程的id,以及对应线程下面request的内容,再将这邪恶内容放入到request对象中。

例如:

在主线程中的变量,如果不隔离,那么在子线程中如果改变了它的值,主线程中的值也就被修改了:

# -*- coding: utf-8 -*-

from threading import Thread

request = 123


class MyThread(Thread):
    def run(self):
        global request
        request = "abc"
        print("子线程", request)


t1 = MyThread()
t1.start()
t1.join()

print("主线程", request)

运行结果:
Web后端学习笔记 Flask(11)Local线程隔离对象_第2张图片

可以看到,主线程中变量request的值,在子线程中被修改,此时变量的值无论是在子线程还是主线程中都发生了变化。

# -*- coding: utf-8 -*-

from threading import Thread
from werkzeug.local import Local

local = Local()
local.request = 123


class MyThread(Thread):
    def run(self):
        local.request = "abc"
        print("子线程", local.request)


t1 = MyThread()
t1.start()
t1.join()

print("主线程", local.request)

Web后端学习笔记 Flask(11)Local线程隔离对象_第3张图片

将request变量绑定到local对象上,此时在不同的线程中,request变量实现了隔离,值相互不影响。

session对象也是绑定在Local对象上,所以它也是线程隔离的:

Web后端学习笔记 Flask(11)Local线程隔离对象_第4张图片

app上下文和request上下文:

app上下文:

@app.route('/')
def hello_world():
    print(current_app.name)
    return 'Hello World!'

在flask中,通过url访问视图函数的时候,flask会自动生成一个app上下文(app_context),然后将app上下文push到一个称为LocalStack()的栈中,而current_app相当于一个指针,始终指向LocalStack()的栈顶元素。

Web后端学习笔记 Flask(11)Local线程隔离对象_第5张图片

Web后端学习笔记 Flask(11)Local线程隔离对象_第6张图片

可以看到,current_app获取LocalStack()栈顶元素的过程

所以当我们在视图函数的外面访问current_app的时候,就会报错,原因是因为此时还没有通过url访问视图函数,LocakStack()中还没有压入任何的app_context,所以此时的current_app指向的是一个空的元素。

在视图函数外面访问app, flask不会动的将app_context压入堆栈,需要先手动创建app上下文,然后手动将其压入LocalStack()栈,才能够通过current_app进行访问。

Web后端学习笔记 Flask(11)Local线程隔离对象_第7张图片

也可以使用with语句创建app_context,更加的方便

request上下文:
在视图函数中,可以通过url_for("视图函数")来对视图函数进行url反转,但是在视图函数之外,如何对视图函数进行url反转?在视图函数之外需要手动创建一个请求上下文,因为在url_for方法开始的地方,需要获取到app_context 和request_context

Web后端学习笔记 Flask(11)Local线程隔离对象_第8张图片

from flask import Flask, request, current_app, url_for
from werkzeug.local import Local
import config

app = Flask(__name__)
app.config.from_object(config)


@app.route('/')
def hello_world():
    print(current_app.name)
    print(url_for("my_list"))   # 视图函数内反转
    return 'Hello World!'


@app.route('/list/')
def my_list():
    return "my list"


with app.test_request_context():
    # 手动推入一个请求上下文到上下文栈中
    # 如果当前应用上下文栈中没有引用上下文
    # 那么首先推入一个应用上下文到栈中
    print(url_for("my_list"))


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

应用上下文和请求上下文都存放到一个LocalStack栈中,和应用app相关的操作就必须用到应用上下文。比如通过current_app获取当前的app.和请求相关的操作就必须使用到请求上下文,例如利用url_for反转视图函数。

1. 在视图函数中,不用担心上下文的问题,因为视图函数要执行,那么一定是通过访问url的方式进行的,此时flask底层已经自动将请求上下文和应用上下文推入到了LocalStack栈中。

2. 如果在视图函数外面需要执行相关的操作,比如获取当前的app或者反转url,那么就必须手动推入相关的上下文。在flask中,可以使用with语句简洁的实现。

为什么上下文需要放在栈中:
1. 应用上下文:Flask底层是基于werkzeug,werkzeug是可以包含多个app,所以用一个栈保存,使用某个app,则这个app应该处于栈顶,app使用完毕,应该从栈中删除。

2. 请求上下文:在写测试代码,或者离线脚本的时候,可能需要创建多个请求上下文,这时候就需要存放到一个栈中。使用哪个请求上下文,就把该请求上下文放到栈顶,用完后从栈中删除。

线程隔离的g对象:

g对象,global的简写。g对象是在整个flask应用运行期间都是可以使用的,并且也跟request一样,是线程隔离的。这个对象是专门用来存储开发者自己定义的一些数据,方便在整个Flask程序中都可以使用。可以将一些常用的数据绑定到上面,以后再使用的时候就可以直接从g上面获取,而不需要通过传参的形式,这样更加方便。

例如:

在common_tools.py中定义一些公共的方法:

# -*- coding: utf-8 -*-
from flask import g


def log_info():
    print("This device {} is working".format(g.device_id))


def log_error():
    print("This device {} is not working".format(g.device_id))

在需要调用这些函数的地方,可以用g对象存储需要的数据,而不需要再通过传参的方式进行:

from flask import Flask, request, current_app, url_for, g
import config
from common_tools import log_info, log_error

app = Flask(__name__)
app.config.from_object(config)


@app.route('/')
def hello_world():
    g.device_id = "#COD_001"
    log_info()
    log_error()
    return 'Hello World!'


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

flask钩子函数:

Flask中钩子函数时使用特定装饰器的函数,为什么叫钩子函数,是因为钩子函数可以在正常执行的代码中,插入一段自己想要执行的代码,那么这种函数就叫做钩子函数。(hook function)

常用的钩子函数:

1. before_first_request: 在flask项目部署之后,第一次请求之前,会调用这个钩子函数,其他情况下不会再调用

@app.before_first_request    # 在请求之前,会先执行这个钩子函数
def first_request():
    print("first request")

2. before_request:在请求发生后,还没有执行视图函数之前,都会先执行这个函数。

@app.before_request    
def before_request_f():
    # 例如在视图函数执行之前,如果这个用户是登陆状态的
    # 可以把跟用户相关的一些信息绑定到g对象上,然后到具体的视图函数中,
    # 就可以使用,g对象中的数据
    user_id = session.get("user_id")
    user_nickname = session.get("user_nickname")
    if user_id:
        g.user_id = user_id    # 存储用户信息到g对象中
        g.user_nickname = user_nickname
    print("first request")

这样做的好处,如果需要在多个视图函数中都需要用到用户的信息,只需要在相应的视图函数中调用g对象即可。如果将获取用户的信息的代码放在试图函数中,那么每一个需要用到这些信息的视图函数,都需要编写一段获取用户信息的代码。

3. template_filter: 在使用jinja2模板的时候,自定义过滤器。

@app.template_filter
def upper_filter(s):
    return s.upper()

4. context_processor, 上下文处理器,在钩子函数中返回的值,在所有模板中都会使用到,且上下文处理器中必须返回字典。例如,在一般需要登陆的网页中,如果处于登陆状态,即使在不同页面之间相互跳转,也会在所有页面上显示用户名。

例如,在两个页面,index和list中,都需要用到用户名:




    
    Title


    

index页面用户名:{{ current_user }}




    
    Title


    

List页面用户名:{{ current_user }}

那么可以在钩子函数,上下文处理器中返回这个变量的值:此时所有的页面都可以使用,而不用在向render_template传递参数。

from flask import Flask, render_template
import config

app = Flask(__name__)
app.config.from_object(config)


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


@app.route('/list/')
def my_list():
    return render_template("html/list.html")


@app.context_processor
def context_process():
    return {"current_user": "tom"}     # 必须返回字典


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

5. errorhandler: 接受状态码,可以自定义返回状态码的相应处理方法。再服务端程序发生异常的时候,比如404,500错误,那么如果想要优雅的处理这些错误,就可以使用errorhandler来完成,例如:

@app.errorhandler(500)   # 服务器内部错误
def server_error(error):
    print(error)
    return "刷新不要太频繁", 500


@app.errorhandler(404)
def page_not_exist(error):
    print(error)
    return "页面不存在了", 404       # 字符串也可以替换为render_template

6. abort(status_code) 可以在flask程序中的任何地方使用,相当于手动抛出一个错误

例如在用户登陆的时候,用户不存在,可以在视图函数中经过判断之后手动abort(400),然后再定义errorhandler(400)的钩子函数来处理这种错误。

Flask中信号机制及使用场景:

 flask中的信号使用的是一个第三方插件,blinker,通过pip install 安装即可,一般是跟随flask同时安装的。

 自定义信号,分为三步:

1. 定义信号:定义信号需要使用到blinker下的Namespace类来创建一个命名空间。比如,定义一个在访问了某个视图函数的时候的信号:需要将自己创建的信号放到命名空间中:

# 定义信号
cx_space = Namespace()         # 创建命名空间
cx_signal = cx_space.signal(name="greet")   # 定义信号

2. 监听信号:监听信号使用signal对象的connect方法,在这个方法中需要传递一个函数,用来做监听到信号以后的操作。

# 监听信号
def greet_func(sender):
    """
    :param sender:  这个参数必须写,表示信号的发送者
    :return:
    """
    print(sender)
    print("hello_fore")
cx_signal.connect(greet_func)

3. 发送信号:发送信号使用signal对象的send方法,这个方法可以传递一些参数过去

# 发送信号
cx_signal.send()

实际应用场景:
定义一个登陆信号,在用户登陆进来以后,就发送一个登陆信号,然后开始监听这个信号,在监听到这个信号之后,就开始记录当前用户的信息,即用信号的方式,记录用户的登陆信息。

# -*- coding: utf-8 -*-

from blinker import Namespace

namespace = Namespace()

login_signal = namespace.signal(name="login")


def login_log(sender):
    print(sender)
    print("用户已经登陆")


login_signal.connect(login_log)      # 监听信号

在视图函数中发送信号:

@app.route('/login/')
def login():
    username = request.args.get("username")     # 通过查询字符串的方式获取参数
    if username:
        login_signal.send()
        return "success login {}".format(username)
    else:
        return "Please Input Username"

对于信号中参数的传递,有两个方案

a.在发送信号的时候,也可以发送参数过去

@app.route('/login/')
def login():
    username = request.args.get("username")     # 通过查询字符串的方式获取参数
    if username:
        login_signal.send(username=username)
        return "success login {}".format(username)
    else:
        return "Please Input Username"
def login_log(sender, username):
    now = datetime.now()
    ip = request.remote_addr    # 获取IP地址
    log_line = "{}|{}|{}".format(username, now, ip)
    with open("log/log.txt", "a") as fp:
        fp.write(log_line + "\n")
    print("用户已经登陆")

b.将参数在登录之后,放入到g对象中,然后发送信号,记录日志,记录日志的函数直接在g对象中调取用户登录信息。

Flask中的内置信号:

1. template_rendered: 模板渲染完成后发送给的信号

from flask import Flask, render_template, request
from flask import template_rendered
import config
from signals import template_rendered_func

app = Flask(__name__)
app.config.from_object(config)

template_rendered.connect(template_rendered_func)    # 模板渲染完成后发送的信号  开始监听信号


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


@app.route('/login/')
def login():
    username = request.args.get("username")     # 通过查询字符串的方式获取参数
    if username:
        return "success login {}".format(username)
    else:
        return "Please Input Username"


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

定义对应的处理函数:

def template_rendered_func(sender, template, context):
    """
    # 函数参数
    :param sender:
    :param template:
    :param context:
    :return:
    """
    print("模板渲染完成")
    print("sender: ", sender)
    print("template: ", template)
    print("context: ", context)

输出信息:

sender:  
template: