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

--------------------------------------------------------------------------------------------------------------------------------

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章