Python+Dash快速web應(yīng)用開發(fā):回調(diào)交互篇(下)

添加微信號(hào)"CNFeffery"加入技術(shù)交流群
?本文示例代碼已上傳至我的
?Github倉庫https://github.com/CNFeffery/DataScienceStudyNotes
1 簡介
這是我的系列教程「Python+Dash快速web應(yīng)用開發(fā)」的第五期,在上一期的文章中,我們針對(duì)Dash中有關(guān)回調(diào)的一些技巧性的特性進(jìn)行了介紹,使得我們可以更愉快地為Dash應(yīng)用編寫回調(diào)交互功能。
而今天的文章作為「回調(diào)交互」系統(tǒng)性內(nèi)容的最后一期,我將帶大家get一些Dash中實(shí)際應(yīng)用效果驚人的「高級(jí)回調(diào)特性」,系好安全帶,我們起飛~

2?Dash中的高級(jí)回調(diào)特性
2.1?控制部分回調(diào)輸出不更新
在很多應(yīng)用場景下,我們給某個(gè)回調(diào)函數(shù)綁定了多個(gè)Output(),這時(shí)如果這些Output()并不是每次觸發(fā)回調(diào)都需要被更新,那么就可以根據(jù)Input()值的不同,來配合dash.no_update作為對(duì)應(yīng)Output()的返回值,從而實(shí)現(xiàn)部分Output()不更新,譬如下面的例子:
?app1.py
?
import?dash
import?dash_bootstrap_components?as?dbc
import?dash_html_components?as?html
from?dash.dependencies?import?Input,?Output
import?time
app?=?dash.Dash(__name__)
app.layout?=?html.Div(
????dbc.Container(
????????[
????????????html.Br(),
????????????html.Br(),
????????????html.Br(),
????????????dbc.Row(
????????????????dbc.Col(
????????????????????dbc.Button('按鈕',
???????????????????????????????color='primary',
???????????????????????????????id='button',
???????????????????????????????n_clicks=0)
????????????????)
????????????),
????????????html.Br(),
????????????dbc.Row(
????????????????[
????????????????????dbc.Col('尚未觸發(fā)',?id='record-1'),
????????????????????dbc.Col('尚未觸發(fā)',?id='record-2'),
????????????????????dbc.Col('尚未觸發(fā)',?id='record-n')
????????????????]
????????????)
????????]
????)
)
@app.callback(
????[Output('record-1',?'children'),
?????Output('record-2',?'children'),
?????Output('record-n',?'children'),
?????],
????Input('button',?'n_clicks'),
????prevent_initial_call=True
)
def?record_click_event(n_clicks):
????if?n_clicks?==?1:
????????return?(
????????????'第1次點(diǎn)擊:{}'.format(time.strftime('%H:%M:%S',?time.localtime(time.time()))),
????????????dash.no_update,
????????????dash.no_update
????????)
????elif?n_clicks?==?2:
????????return?(
????????????dash.no_update,
????????????'第2次點(diǎn)擊:{}'.format(time.strftime('%H:%M:%S',?time.localtime(time.time()))),
????????????dash.no_update
????????)
????elif?n_clicks?>=?3:
????????return?(
????????????dash.no_update,
????????????dash.no_update,
????????????'第3次及以上點(diǎn)擊:{}'.format(time.strftime('%H:%M:%S',?time.localtime(time.time()))),
????????)
if?__name__?==?'__main__':
????app.run_server(debug=True)

可以觀察到,我們根據(jù)n_clicks數(shù)值的不同,在對(duì)應(yīng)各個(gè)Output()返回值中對(duì)符合條件的部件進(jìn)行更新,其他的都用dash.no_update來代替,從而實(shí)現(xiàn)了局部更新,非常實(shí)用且簡單。
2.2 基于模式匹配的回調(diào)
這是Dash在1.11.0版本開始引入的新特性,它所實(shí)現(xiàn)的功能是將多個(gè)部件綁定組織在同一個(gè)id屬性下,這聽起來有一點(diǎn)抽象,我們先從一個(gè)形象的例子來出發(fā):
假如我們要開發(fā)一個(gè)簡單的「記賬」應(yīng)用,它通過第一排若干Input()部件及一個(gè)Button()部件來記錄并提交每筆賬對(duì)應(yīng)的相關(guān)信息,并且在最下方輸出已記錄賬目金額之和:
?app2.py
?
import?dash
import?dash_bootstrap_components?as?dbc
import?dash_core_components?as?dcc
import?dash_html_components?as?html
from?dash.dependencies?import?Input,?Output,?State,?ALL
import?re
app?=?dash.Dash(__name__)
app.layout?=?html.Div(
????[
????????html.Br(),
????????html.Br(),
????????dbc.Container(
????????????dbc.Row(
????????????????[
????????????????????dbc.Col(
????????????????????????dbc.InputGroup(
????????????????????????????[
????????????????????????????????dbc.InputGroupAddon("金額",?addon_type="prepend"),
????????????????????????????????dbc.Input(
????????????????????????????????????id='account-amount',
????????????????????????????????????placeholder='請輸入金額',
????????????????????????????????????type="number",
????????????????????????????????),
????????????????????????????????dbc.InputGroupAddon("元",?addon_type="append"),
????????????????????????????],
????????????????????????),
????????????????????????width=5
????????????????????),
????????????????????dbc.Col(
????????????????????????dcc.Dropdown(
????????????????????????????id='account-type',
????????????????????????????options=[
????????????????????????????????{'label':?'生活開銷',?'value':?'生活開銷'},
????????????????????????????????{'label':?'人情往來',?'value':?'人情往來'},
????????????????????????????????{'label':?'醫(yī)療保健',?'value':?'醫(yī)療保健'},
????????????????????????????????{'label':?'旅游休閑',?'value':?'旅游休閑'},
????????????????????????????],
????????????????????????????placeholder='請選擇類型:'
????????????????????????),
????????????????????????width=5
????????????????????),
????????????????????dbc.Col(
????????????????????????dbc.Button('提交記錄',?id='account-submit'),
????????????????????????width=2
????????????????????)
????????????????]
????????????)
????????),
????????html.Br(),
????????dbc.Container([],?id='account-record-container'),
????????dbc.Container('暫無記錄!',?id='account-record-sum')
????]
)
@app.callback(
????Output('account-record-container',?'children'),
????Input('account-submit',?'n_clicks'),
????[State('account-record-container',?'children'),
?????State('account-amount',?'value'),
?????State('account-type',?'value')],
????prevent_initial_call=True
)
def?update_account_records(n_clicks,?children,?account_amount,?account_type):
????'''
????用于處理每一次的記賬輸入并渲染前端記錄
????'''
????if?account_amount?and?account_type:
????????children.append(dbc.Row(
????????????dbc.Col(
????????????????'【{}】類開銷【{}】元'.format(account_type,?account_amount)
????????????),
????????????#?以字典形式定義id
????????????id={'type':?'single-account_record',?'index':?children.__len__()}
????????))
????????return?children
@app.callback(
????Output('account-record-sum',?'children'),
????Input({'type':?'single-account_record',?'index':?ALL},?'children'),
????prevent_initial_call=True
)
def?refresh_account_sum(children):
????'''
????對(duì)多部件集合single-account_record下所有賬目記錄進(jìn)行求和
????'''
????return?'賬本總開銷:{}'.format(sum([int(re.findall('\d+',
?????????????????????????????????????????????????child['props']['children'])[0])
??????????????????????????????????for?child?in?children]))
if?__name__?==?'__main__':
????app.run_server(debug=True)

上面這個(gè)應(yīng)用中,體現(xiàn)出的「模式匹配」內(nèi)容即為開頭從dash.dependencies引入的ALL,它是Dash「模式匹配」中的一種模式,而我們在回調(diào)函數(shù)update_account_records()中為已有記賬記錄追加新紀(jì)錄時(shí),使用到:
#?以字典形式定義id
id={'type':?'single-account_record',?'index':?children.__len__()}
這里不同于以前我們采取的id=某個(gè)字符串的定義方法,換成字典之后,其type鍵值對(duì)用來記錄唯一id信息,每一次新紀(jì)錄追加時(shí)type值都相等,因?yàn)樗鼈儽唤M織為「同id部件集合」,而鍵值對(duì)index則用于在type值相同的一個(gè)部件集合下,區(qū)分出不同的獨(dú)立部件元素。
因?yàn)閷鹘y(tǒng)的「唯一id部件」替換成「同id部件集合」,所以我們后面的回調(diào)函數(shù)refresh_account_sum()的輸入元素只需要定義單個(gè)Input()即可,再在函數(shù)內(nèi)部按照不同的index值取出需要的集合內(nèi)各成員記錄值,非常便于我們書寫出簡練清爽的Dash代碼,便于之后進(jìn)一步的修改與重構(gòu)。
你可以通過最下面打印出的每次refresh_account_sum()所接收到的children參數(shù)json格式結(jié)果來弄清我是如何在return值的地方取出歷史記賬金額并計(jì)算的。
而除了上面介紹的一股腦返回所有集合內(nèi)成員部件的ALL模式之外,還有另一種更有針對(duì)性的MATCH模式,它應(yīng)用于結(jié)合內(nèi)成員部件可交互輸入值的情況,譬如下面這個(gè)簡單的例子,我們定義一個(gè)簡單的用于查詢省份行政代碼的應(yīng)用,配合MATCH模式來實(shí)現(xiàn)彼此成對(duì)獨(dú)立輸出:
?app3.py
?
import?dash
import?dash_bootstrap_components?as?dbc
import?dash_html_components?as?html
from?dash.dependencies?import?Input,?Output,?State,?MATCH
import?dash_core_components?as?dcc
app?=?dash.Dash(__name__)
app.layout?=?html.Div(
????[
????????html.Br(),
????????html.Br(),
????????html.Br(),
????????dbc.Container(
????????????[
????????????????dbc.Row(
????????????????????dbc.Col(
????????????????????????dbc.Button('新增查詢',?id='add-item',?outline=True)
????????????????????)
????????????????),
????????????????html.Hr()
????????????]
????????),
????????dbc.Container([],?id='query-container')
????]
)
region2code?=?{
????'北京市':?'110000000000',
????'重慶市':?'500000000000',
????'安徽省':?'340000000000'
}
@app.callback(
????Output('query-container',?'children'),
????Input('add-item',?'n_clicks'),
????State('query-container',?'children'),
????prevent_initial_call=True
)
def?add_query_item(n_clicks,?children):
????children.append(
????????dbc.Row(
????????????[
????????????????dbc.Col(
????????????????????[
????????????????????????#?生成index相同的dropdown部件與文字輸出部件
????????????????????????dcc.Dropdown(id={'type':?'select-province',?'index':?children.__len__()},
?????????????????????????????????????options=[{'label':?label,?'value':?label}?for?label?in?region2code.keys()],
?????????????????????????????????????placeholder='選擇省份:'),
????????????????????????html.P('請輸入要查詢的省份!',?id={'type':?'code-output',?'index':?children.__len__()})
????????????????????]
????????????????)
????????????]
????????)
????)
????return?children
@app.callback(
????Output({'type':?'code-output',?'index':?MATCH},?'children'),
????Input({'type':?'select-province',?'index':?MATCH},?'value')
)
def?refresh_code_output(value):
????if?value:
????????return?region2code[value]
????else:
????????return?dash.no_update
if?__name__?==?'__main__':
????app.run_server(debug=True)

可以看到,在refresh_code_output()前應(yīng)用MATCH模式匹配后,我們點(diǎn)擊某個(gè)部件時(shí),只有跟它index匹配的部件才會(huì)打印出相對(duì)應(yīng)的輸出,非常的方便~
2.3 多輸入情況下獲取部件觸發(fā)情況
在很多應(yīng)用場景下,我們的某個(gè)回調(diào)可能擁有多個(gè)Input輸入,但學(xué)過前面的內(nèi)容我們已經(jīng)清楚,不管有幾個(gè)Input,只要其中有一個(gè)部件其輸入屬性發(fā)生變化,都會(huì)觸發(fā)本輪回調(diào),但是如果我們就想知道究竟是「哪個(gè)」Input觸發(fā)了本輪回調(diào)該怎么辦呢?
這在Dash中可以通過dash.callback_context來方便的實(shí)現(xiàn),它只能在回調(diào)函數(shù)中被執(zhí)行,從而獲取回調(diào)過程的諸多上下文信息,先從下面這個(gè)簡單的例子出發(fā)看看dash.callback_context到底給我們帶來了哪些有價(jià)值的信息:
?app4.py
?
import?dash
import?dash_html_components?as?html
import?dash_bootstrap_components?as?dbc
from?dash.dependencies?import?Input,?Output
import?json
app?=?dash.Dash(__name__)
app.layout?=?html.Div(
????dbc.Container(
????????[
????????????html.Br(),
????????????html.Br(),
????????????html.Br(),
????????????dbc.Row(
????????????????[
????????????????????dbc.Col(dbc.Button('A',?id='A',?n_clicks=0)),
????????????????????dbc.Col(dbc.Button('B',?id='B',?n_clicks=0)),
????????????????????dbc.Col(dbc.Button('C',?id='C',?n_clicks=0))
????????????????]
????????????),
????????????dbc.Row(
????????????????[
????????????????????dbc.Col(html.P('按鈕A未點(diǎn)擊',?id='A-output')),
????????????????????dbc.Col(html.P('按鈕B未點(diǎn)擊',?id='B-output')),
????????????????????dbc.Col(html.P('按鈕C未點(diǎn)擊',?id='C-output'))
????????????????]
????????????),
????????????dbc.Row(
????????????????dbc.Col(
????????????????????html.Pre(id='raw-json')
????????????????)
????????????)
????????]
????)
)
@app.callback(
????[Output('A-output',?'children'),
?????Output('B-output',?'children'),
?????Output('C-output',?'children'),
?????Output('raw-json',?'children')],
????[Input('A',?'n_clicks'),
?????Input('B',?'n_clicks'),
?????Input('C',?'n_clicks')],
????prevent_initial_call=True
)
def?refresh_output(A_n_clicks,?B_n_clicks,?C_n_clicks):
????#?獲取本輪回調(diào)狀態(tài)下的上下文信息
????ctx?=?dash.callback_context
????#?取出對(duì)應(yīng)State、最近一次觸發(fā)部件以及Input信息
????ctx_msg?=?json.dumps({
????????'states':?ctx.states,
????????'triggered':?ctx.triggered,
????????'inputs':?ctx.inputs
????},?indent=2)
????return?A_n_clicks,?B_n_clicks,?C_n_clicks,?ctx_msg
if?__name__?==?'__main__':
????app.run_server(debug=True)

可以看到,我們安插在回調(diào)函數(shù)里的dash.callback_context幫我們記錄了從訪問Dash開始,到最近一次執(zhí)行回調(diào)期間,對(duì)應(yīng)回調(diào)的輸入輸出信息變化情況、最近一次觸發(fā)信息,非常的實(shí)用,可以支撐起很多復(fù)雜應(yīng)用場景。
2.4 在瀏覽器端執(zhí)行回調(diào)過程
Dash雖然很方便,使得我們可以完全不用書寫js代碼就可以實(shí)現(xiàn)各種回調(diào)交互,但把所有的交互響應(yīng)計(jì)算過程都交給服務(wù)端來做,省事倒是很省事,但會(huì)給服務(wù)器帶來不小的計(jì)算和網(wǎng)絡(luò)傳輸壓力。
因此很多容易頻繁觸發(fā)且與主要的數(shù)值計(jì)算無關(guān)的交互行為,完全可以搬到瀏覽器端執(zhí)行,既快速又不吃服務(wù)器的計(jì)算資源,這也是當(dāng)初JavaScript被發(fā)明的一個(gè)重要原因,而在Dash中,也為略懂js的用戶提供了在瀏覽器端執(zhí)行一些回調(diào)的貼心功能。
從一個(gè)很簡單的點(diǎn)擊按鈕,實(shí)現(xiàn)部分網(wǎng)頁內(nèi)容的打開與關(guān)閉出發(fā),這里我們提前使用到dbc.Collapse部件,用于將所包含的網(wǎng)頁內(nèi)容與其它按鈕部件的點(diǎn)擊行為進(jìn)行綁定:
?app5.py
?
import?dash
import?dash_bootstrap_components?as?dbc
import?dash_html_components?as?html
from?dash.dependencies?import?Input,?Output,?State
app?=?dash.Dash(__name__)
app.layout?=?html.Div(
????dbc.Container(
????????[
????????????html.Br(),
????????????html.Br(),
????????????html.Br(),
????????????dbc.Button('服務(wù)端回調(diào)',?id='server-button'),
????????????dbc.Collapse('服務(wù)端折疊內(nèi)容',?id='server-collapse'),
????????????html.Hr(),
????????????dbc.Button('瀏覽器端回調(diào)',?id='browser-button'),
????????????dbc.Collapse('瀏覽器端折疊內(nèi)容',?id='browser-collapse'),
????????]
????)
)
@app.callback(
????Output('server-collapse',?'is_open'),
????Input('server-button',?'n_clicks'),
????State('server-collapse',?'is_open'),
????prevent_initial_call=True
)
def?server_callback(n_clicks,?is_open):
????return?not?is_open
#?在dash中定義瀏覽器端回調(diào)函數(shù)的特殊格式
app.clientside_callback(
????"""
????function(n_clicks,?is_open)?{
????????return?!is_open;
????}
????""",
????Output('browser-collapse',?'is_open'),
????Input('browser-button',?'n_clicks'),
????State('browser-collapse',?'is_open'),
????prevent_initial_call=True
)
if?__name__?==?'__main__':
????app.run_server(debug=True)
可以看到,服務(wù)端回調(diào)我們照常寫,而瀏覽器端回調(diào)通過傳入一個(gè)非常簡單的js函數(shù),在每次回調(diào)時(shí)接受輸入并輸出is_open的邏輯反值,從而實(shí)現(xiàn)了折疊內(nèi)容的打開與關(guān)閉切換:
function(n_clicks,?is_open)?{
????????return?!is_open;
}
便實(shí)現(xiàn)了瀏覽器端回調(diào)!

而如果你想要執(zhí)行的瀏覽器端js回調(diào)函數(shù)代碼有點(diǎn)長,還可以按照下圖格式,把你的大段js回調(diào)函數(shù)代碼放置于assets目錄下對(duì)應(yīng)路徑里的js腳本中:

接著再在dash中按照下列格式編寫關(guān)聯(lián)輸入輸出與上述js回調(diào)的簡短語句即可:
app.clientside_callback(
????ClientsideFunction(
????????namespace='命名空間名稱',
????????function_name='對(duì)應(yīng)js回調(diào)函數(shù)名'
????),
????'''
????按順序組織你的Output、Input以及State...?...
????'''
)
下面我們直接以大家喜聞樂見的數(shù)據(jù)可視化頂級(jí)框架echarts為例,來寫一個(gè)根據(jù)不同輸入值切換渲染出的圖表類型,「注意」請從官網(wǎng)把依賴的echarts.min.js下載到我們的assets路徑下對(duì)應(yīng)位置,它會(huì)在我們的Dash應(yīng)用啟動(dòng)時(shí)與所有assets下的資源一起自動(dòng)被載入到瀏覽器中:
?app6.py
?
import?dash
import?dash_bootstrap_components?as?dbc
import?dash_html_components?as?html
import?dash_core_components?as?dcc
from?dash.dependencies?import?Input,?Output,?ClientsideFunction
app?=?dash.Dash(__name__)
#?編寫一個(gè)根據(jù)dropdown不同輸入值切換對(duì)應(yīng)圖表類型的小應(yīng)用
app.layout?=?html.Div(
????dbc.Container(
????????[
????????????html.Br(),
????????????dbc.Row(
????????????????dbc.Col(
????????????????????dcc.Dropdown(
????????????????????????id='chart-type',
????????????????????????options=[
????????????????????????????{'label':?'折線圖',?'value':?'折線圖'},
????????????????????????????{'label':?'堆積面積圖',?'value':?'堆積面積圖'},
????????????????????????],
????????????????????????value='折線圖'
????????????????????),
????????????????????width=3
????????????????)
????????????),
????????????html.Br(),
????????????dbc.Row(
????????????????dbc.Col(
????????????????????html.Div(
????????????????????????html.Div(
????????????????????????????id='main',
????????????????????????????style={
????????????????????????????????'height':?'100%',
????????????????????????????????'width':?'100%'
????????????????????????????}
????????????????????????),
????????????????????????style={
????????????????????????????'width':?'800px',
????????????????????????????'height':?'500px'
????????????????????????}
????????????????????)
????????????????)
????????????)
????????]
????)
)
app.clientside_callback(
????#?關(guān)聯(lián)自編js腳本中的相應(yīng)回調(diào)函數(shù)
????ClientsideFunction(
????????namespace='clientside',
????????function_name='switch_chart'
????),
????Output('main',?'children'),
????Input('chart-type',?'value')
)
if?__name__?==?'__main__':
????app.run_server(debug=True)

效果十分驚人,從此我們使用Dash不僅僅可以使用Python生態(tài)的工具,還可以配合對(duì)前端內(nèi)容支持更好的js,起飛!
至此我們的Dash回調(diào)交互三部曲已結(jié)束,接下來的文章我將開始帶大家遨游豐富的各種Dash前端部件,涵蓋了網(wǎng)頁部件、數(shù)據(jù)可視化圖表以及地圖可視化等內(nèi)容,敬請期待這場奇妙之旅吧~
以上就是本文的全部內(nèi)容,歡迎在評(píng)論區(qū)與我進(jìn)行討論。

加入知識(shí)星球【我們談?wù)摂?shù)據(jù)科學(xué)】
300+小伙伴一起學(xué)習(xí)!
· 推薦閱讀?·
妙啊,速來get這9個(gè)jupyter實(shí)用技巧!
Python+Dash快速web應(yīng)用開發(fā):回調(diào)交互篇(中)
在模仿中精進(jìn)數(shù)據(jù)可視化07:星球研究所大壩分布可視化
