Web后端学习笔记 Flask(11)Local线程隔离对象
flask中的上下文:应用上下文和请求上下文
1. 在flask中,是通过request对象获取用户提交的数据,但是在整个程序运行中,只有一个request对象。在实际应用场景中,会有多个用户同时进行数据提交。此时应该开多个子线程,或者协程进行处理(即有多个request独立对象)。在Flask中通过Local解决这一问题。
只要绑定在Local对象上的属性,在每个线程中都是隔离的
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)
运行结果:
可以看到,主线程中变量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)
将request变量绑定到local对象上,此时在不同的线程中,request变量实现了隔离,值相互不影响。
session对象也是绑定在Local对象上,所以它也是线程隔离的:
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()的栈顶元素。
可以看到,current_app获取LocalStack()栈顶元素的过程
所以当我们在视图函数的外面访问current_app的时候,就会报错,原因是因为此时还没有通过url访问视图函数,LocakStack()中还没有压入任何的app_context,所以此时的current_app指向的是一个空的元素。
在视图函数外面访问app, flask不会动的将app_context压入堆栈,需要先手动创建app上下文,然后手动将其压入LocalStack()栈,才能够通过current_app进行访问。
也可以使用with语句创建app_context,更加的方便
request上下文:
在视图函数中,可以通过url_for("视图函数")来对视图函数进行url反转,但是在视图函数之外,如何对视图函数进行url反转?在视图函数之外需要手动创建一个请求上下文,因为在url_for方法开始的地方,需要获取到app_context 和request_context
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中,都需要用到用户名:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p>index页面用户名:{{ current_user }}</p>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p>List页面用户名:{{ current_user }}</p>
</body>
</html>
那么可以在钩子函数,上下文处理器中返回这个变量的值:此时所有的页面都可以使用,而不用在向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: <Flask 'app'>
template: <Template 'html/index.html'>
context: {'g': <flask.g of 'app'>, 'request': <Request 'http://127.0.0.1:5000/' [GET]>, 'session': <SecureCookieSession {}>}
2. got_request_exception: 视图函数中发生异常时发送的信息
got_request_exception.connect(got_request_exception_func)
def got_request_exception_func(sender, *args, **kwargs):
"""
:param sender:
:param args:
:param kwargs:
:return:
"""
print(sender)
print(args)
print(kwargs)
输出信息:关键字参数以及位置参数的输出
<Flask 'app'>
()
{'exception': ZeroDivisionError('division by zero',)}
Flask所有的内置信号:
# Core signals. For usage examples grep the source code or consult # the API documentation in docs/api.rst as well as docs/signals.rst 1. template_rendered: 模板渲染完成后发送的信号 2. before_render_template :模板渲染前发送的信号 3. request_started :模板开始渲染 request_finished :模板渲染完成 request_tearing_down :对象被销毁的信号 got_request_exception :视图函数发生异常的信号,一般可以监听这个信号来记录网站异常信息 appcontext_tearing_down :app上下文被销毁的信号 appcontext_pushed :app上下文被推入LocalStack栈的信号 appcontext_popped :app上下文被推出LocalStack栈的信号 message_flashed :调用了Flask的“flashed”的信号
--------------------------------------------------------------------------------------------------------------------------------