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”的信號
--------------------------------------------------------------------------------------------------------------------------------