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

添加微信號"CNFeffery"加入技術(shù)交流群
?本文示例代碼已上傳至我的
?Github倉庫https://github.com/CNFeffery/DataScienceStudyNotes
1 簡介
這是我的系列教程「Python+Dash快速web應(yīng)用開發(fā)」的第四期,在上一期的文章中,我們進(jìn)入了Dash核心內(nèi)容——callback,get到如何在不編寫js代碼的情況下,輕松實(shí)現(xiàn)前后端異步通信,為創(chuàng)造任意交互方式的Dash應(yīng)用打下基礎(chǔ)。
而在今天的文章中,我將帶大家學(xué)習(xí)有關(guān)Dash中「回調(diào)」的一些非常實(shí)用,且不算復(fù)雜的額外特性,讓你更加熟悉Dash的回調(diào)交互~

2 Dash中的回調(diào)實(shí)用小特性
2.1 靈活使用debug模式
開發(fā)階段,在Dash中使用run_server()啟動我們的應(yīng)用時,可以添加參數(shù)debug=True來切換為「debug」模式,在這種模式下,我們可以獲得以下輔助功能:
「熱重載」
熱重載指的是,我們在編寫完一個Dash的完整應(yīng)用并在debug模式下啟動之后,在保持應(yīng)用運(yùn)行的情況下,修改源代碼并保存之后,瀏覽器中運(yùn)行的Dash實(shí)例會自動重啟刷新,就像下面的例子一樣:
?app1.py
?
import?dash
import?dash_html_components?as?html
app?=?dash.Dash(__name__)
app.layout?=?html.Div(
????html.H1('我是熱重載之前!')
)
if?__name__?==?'__main__':
????app.run_server(debug=True)

可以看到,debug模式下,我們對源代碼做出的修改在保存之后,都會受到Dash的監(jiān)聽,從而做出反饋(注意一定要在作出修改的代碼完整之后再保存,否則代碼寫到一半就保存會引起語法錯誤等中斷當(dāng)前Dash實(shí)例)。
「對回調(diào)結(jié)構(gòu)進(jìn)行可視化」
你可能已經(jīng)注意到,在開啟debug模式之后,我們?yōu)g覽器中的Dash應(yīng)用右下角出現(xiàn)的藍(lán)色logo,點(diǎn)擊打開折疊,可以看到幾個按鈕:

其中第一個「Callbacks」非常有意思,它可以幫助我們對當(dāng)前Dash應(yīng)用中的回調(diào)關(guān)系進(jìn)行可視化,譬如下面的例子:
?app2.py
?
import?dash
import?dash_bootstrap_components?as?dbc
import?dash_html_components?as?html
from?dash.dependencies?import?Input,?Output
app?=?dash.Dash(
????__name__,
????external_stylesheets=['css/bootstrap.min.css']
)
app.layout?=?html.Div(
????dbc.Container(
????????[
????????????html.Br(),
????????????html.Br(),
????????????html.Br(),
????????????dbc.Row(
????????????????[
????????????????????dbc.Col(
????????????????????????dbc.Input(id='input1'),
????????????????????????width=4
????????????????????),
????????????????????dbc.Col(
????????????????????????dbc.Label(id='output1'),
????????????????????????width=4
????????????????????)
????????????????]
????????????),
????????????dbc.Row(
????????????????[
????????????????????dbc.Col(
????????????????????????dbc.Input(id='input2'),
????????????????????????width=4
????????????????????),
????????????????????dbc.Col(
????????????????????????dbc.Label(id='output2'),
????????????????????????width=4
????????????????????)
????????????????]
????????????)
????????]
????)
)
@app.callback(
????Output('output1',?'children'),
????Input('input1',?'value')
)
def?callback1(value):
????if?value:
????????return?int(value)?**?2
@app.callback(
????Output('output2',?'children'),
????Input('input2',?'value')
)
def?callback2(value):
????if?value:
????????return?int(value)?**?0.5
if?__name__?==?"__main__":
????app.run_server(debug=True)

可以看到,我們打開「Callbacks」之后,可以看到每個回調(diào)的輸入輸出、通信延遲等信息,可以幫助我們更有條理的組織各個回調(diào)。
「展示運(yùn)行錯誤信息」
既然主要功能是debug,自然是可以幫助我們在程序出現(xiàn)錯誤時打印具體的錯誤信息,我們在前面app2.py例子的基礎(chǔ)上,故意制造一些錯誤(此處代碼粘貼有誤,請查看評論區(qū)說明):
?app3.py
?
import?dash
import?dash_bootstrap_components?as?dbc
import?dash_core_components?as?dcc
import?dash_html_components?as?html
app?=?dash.Dash(
????__name__,
????external_stylesheets=['css/bootstrap.min.css']
)
app.layout?=?html.Div(
????[
????????#?fluid默認(rèn)為False
????????dbc.Container(
????????????[
????????????????dcc.Dropdown(),
????????????????'測試',
????????????????dcc.Dropdown()
????????????]
????????),
????????html.Hr(),?#?水平分割線
????????#?fluid設(shè)置為True
????????dbc.Container(
????????????[
????????????????dcc.Dropdown(),
????????????????'測試',
????????????????dcc.Dropdown()
????????????],
????????????fluid=True
????????)
????]
)
if?__name__?==?"__main__":
????app.run_server()

可以看到,我們故意制造出的兩種錯誤:「不處理Input()默認(rèn)的缺失值value」、「Output()傳入不存在的id」,都在瀏覽器中得到輸出,并且可自由查看錯誤信息,這對我們開發(fā)過程幫助很大。
2.2 阻止應(yīng)用的初始回調(diào)
在前面的app3例子中,我們故意制造出的錯誤之一是「不處理Input()默認(rèn)的缺失值value」,這里的錯誤展開來說是因?yàn)?code style="margin-right: 2px;margin-left: 2px;padding: 2px 4px;font-size: 14px;overflow-wrap: break-word;border-radius: 4px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">Input()部件value屬性的默認(rèn)值是None,使得剛載入應(yīng)用還未輸入值時引發(fā)了回調(diào)中計(jì)算部分的邏輯錯誤。
類似這樣的情況很多,可以通過給部件相應(yīng)屬性設(shè)置默認(rèn)值或者在回調(diào)中寫條件判斷等方式處理,就像app2中那樣,但如果這樣的部件比較多,一個一個逐一處理還是比較繁瑣,而Dash中提供了「阻止初始回調(diào)」的特性,只需要在app.callback裝飾器中設(shè)置參數(shù)prevent_initial_call=True即可:
?app4.py
?
import?dash
import?dash_bootstrap_components?as?dbc
import?dash_html_components?as?html
from?dash.dependencies?import?Input,?Output
app?=?dash.Dash(
????__name__,
????external_stylesheets=['css/bootstrap.min.css']
)
app.layout?=?html.Div(
????dbc.Container(
????????[
????????????html.Br(),
????????????html.Br(),
????????????html.Br(),
????????????dbc.Row(
????????????????[
????????????????????dbc.Col(
????????????????????????dbc.Input(id='input1'),
????????????????????????width=4
????????????????????),
????????????????????dbc.Col(
????????????????????????dbc.Label(id='output1'),
????????????????????????width=4
????????????????????)
????????????????]
????????????)
????????]
????)
)
@app.callback(
????Output('output1',?'children'),
????Input('input1',?'value'),
????prevent_initial_call=True
)
def?callback1(value):
????return?int(value)?**?2
if?__name__?==?"__main__":
????app.run_server(debug=True)

可以看到,設(shè)置完參數(shù)后,Dash應(yīng)用被訪問時,不會自動執(zhí)行首次回調(diào),非常的方便。
2.3 忽略回調(diào)匹配錯誤
在前面我們還制造出了「Output()傳入不存在的id」這種錯誤,也就是回調(diào)函數(shù)查找輸入輸出等關(guān)系時,出現(xiàn)匹配失敗的情況。
但在很多時候,我們需要在發(fā)生某些交互回調(diào)時,才創(chuàng)建返回一些具有指定「id」的部件,這時如果程序中提前寫好了針對這些初始化時「不存在」的部件的回調(diào),就會觸發(fā)前面的錯誤。
在Dash中提供了解決此類問題的方法,在創(chuàng)建app實(shí)例時添加參數(shù)suppress_callback_exceptions=True即可:
?app5.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
app?=?dash.Dash(
????__name__,
????external_stylesheets=['css/bootstrap.min.css'],
????#?suppress_callback_exceptions=True
)
app.layout?=?html.Div(
????dbc.Container(
????????[
????????????dbc.Row(
????????????????[
????????????????????dbc.Col(
????????????????????????dbc.Input(id='input_num')
????????????????????),
????????????????????dbc.Col(id='output_item')
????????????????]
????????????),
????????????dbc.Row(
????????????????dbc.Col(
????????????????????dbc.Label(id='output_desc')
????????????????)
????????????)
????????]
????)
)
@app.callback(
????Output('output_item',?'children'),
????Input('input_num',?'value'),
????prevent_initial_call=True
)
def?callback1(value):
????return?dcc.Dropdown(
????????id='output_dropdown',
????????options=[
????????????{'label':?i,?'value':?i}
????????????for?i?in?range(int(value))
????????]
????)
@app.callback(
????Output('output_desc',?'children'),
????Input('output_dropdown',?'options'),
????prevent_initial_call=True
)
def?callback2(options):
????return?'生成的Dropdown部件共有{}個選項(xiàng)'.format(options.__len__())
if?__name__?==?"__main__":
????app.run_server(debug=True)

可以看到,參數(shù)添加后,Dash會自動忽略類似的回調(diào)匹配錯誤,非常的實(shí)用,這個知識點(diǎn)我們會在以后的「前后端分離」篇中頻繁地使用到,所以一定要記住它。
3 編寫一個貸款計(jì)算器
get完今天所學(xué)的知識點(diǎn)后,我們通過實(shí)際的例子,來鞏固上一期及這一期的內(nèi)容,幫助大家對Dash中的回調(diào)基礎(chǔ)知識有更好的理解。
今天我們要編寫的例子,是貸款計(jì)算器,要編寫出一個實(shí)際的貸款計(jì)算器,我們需要組織以下用戶輸入內(nèi)容:
「貸款總金額」 「還款月份數(shù)量」 「年利率」 「還款方式」
其中還款方式主要有「等額本息」與「等額本金」兩種,我們利用之前介紹過的dash-bootstrap-components來搭建頁面,其中「貸款金額」、「還款月份數(shù)量」以及「年利率」我們都使用Input()部件來實(shí)現(xiàn),并利用參數(shù)type="number"來約束其類型為數(shù)值。
而「還款方式」是二選一,所以我們使用部件RadioItems()來實(shí)現(xiàn),最后設(shè)置計(jì)算按鈕,配合以前介紹過的State()和n_clicks來交互執(zhí)行計(jì)算,并以plotly.express折線圖的形式呈現(xiàn)計(jì)算結(jié)果(這部分我們將在之后的「嵌入可視化」中詳細(xì)介紹),最終得到的效果如下:

代碼如下:
?app6.py
?
import?dash
import?dash_html_components?as?html
import?plotly.express?as?px
import?dash_core_components?as?dcc
import?dash_bootstrap_components?as?dbc
from?dash.dependencies?import?Output,?Input,?State
import?time
app?=?dash.Dash(
????__name__,
????external_stylesheets=['css/bootstrap.min.css'],
????suppress_callback_exceptions=True
)
app.layout?=?html.Div(
????dbc.Container(
????????[
????????????html.Br(),
????????????html.Br(),
????????????html.Br(),
????????????html.Br(),
????????????dbc.Row(
????????????????dbc.Col(
????????????????????dbc.InputGroup(
????????????????????????[
????????????????????????????dbc.InputGroupAddon("貸款金額",?addon_type="prepend"),
????????????????????????????dbc.Input(
????????????????????????????????id='loan_amount',
????????????????????????????????placeholder='請輸入貸款總金額',
????????????????????????????????type="number",
????????????????????????????????value=100
????????????????????????????),
????????????????????????????dbc.InputGroupAddon("萬元",?addon_type="append"),
????????????????????????],
????????????????????),
????????????????????width={'size':?6,?'offset':?3}
????????????????)
????????????),
????????????html.Br(),
????????????dbc.Row(
????????????????dbc.Col(
????????????????????dbc.InputGroup(
????????????????????????[
????????????????????????????dbc.InputGroupAddon("計(jì)劃還款月數(shù)",?addon_type="prepend"),
????????????????????????????dbc.Input(
????????????????????????????????id='repay_month_amount',
????????????????????????????????placeholder='請輸入計(jì)劃還款月數(shù)',
????????????????????????????????type="number",
????????????????????????????????value=24,
????????????????????????????????min=1,
????????????????????????????????step=1
????????????????????????????),
????????????????????????????dbc.InputGroupAddon("個月",?addon_type="append"),
????????????????????????],
????????????????????),
????????????????????width={'size':?6,?'offset':?3}
????????????????)
????????????),
????????????html.Br(),
????????????dbc.Row(
????????????????dbc.Col(
????????????????????dbc.InputGroup(
????????????????????????[
????????????????????????????dbc.InputGroupAddon("年利率",?addon_type="prepend"),
????????????????????????????dbc.Input(
????????????????????????????????id='interest_rate',
????????????????????????????????placeholder='請輸入年利率',
????????????????????????????????type="number",
????????????????????????????????value=5,
????????????????????????????????min=0,
????????????????????????????????step=0.001
????????????????????????????),
????????????????????????????dbc.InputGroupAddon("%",?addon_type="append"),
????????????????????????],
????????????????????),
????????????????????width={'size':?6,?'offset':?3}
????????????????)
????????????),
????????????html.Br(),
????????????dbc.Row(
????????????????dbc.Col(
????????????????????dbc.RadioItems(
????????????????????????id="repay_method",
????????????????????????options=[
????????????????????????????{"label":?"等額本息",?"value":?"等額本息"},
????????????????????????????{"label":?"等額本金",?"value":?"等額本金"}
????????????????????????],
????????????????????????value='等額本息'
????????????????????),
????????????????????width={'size':?6,?'offset':?3}
????????????????),
????????????),
????????????html.Br(),
????????????dbc.Row(
????????????????dbc.Col(
????????????????????dbc.Button('開始計(jì)算',?id='start',?n_clicks=0,?color='light'),
????????????????????width={'size':?6,?'offset':?3}
????????????????),
????????????),
????????????html.Br(),
????????????dbc.Row(
????????????????dbc.Col(
????????????????????dcc.Loading(dcc.Graph(id='repay_timeline')),
????????????????????width={'size':?6,?'offset':?3}
????????????????),
????????????),
????????],
????????fluid=True
????)
)
def?make_line_graph(loan_amount,
????????????????????repay_month_amount,
????????????????????interest_rate,
????????????????????repay_method):
????interest_rate?/=?100
????loan_amount?*=?10000
????month_interest_rate?=?interest_rate?/?12
????if?repay_method?==?'等額本息':
????????month_repay?=?loan_amount?*?month_interest_rate?*?pow((1?+?month_interest_rate),?repay_month_amount)?/?\
??????????????????????(pow((1?+?month_interest_rate),?repay_month_amount)?-?1)
????????month_repay?=?round(month_repay,?2)
????????month_repay?=?[month_repay]?*?repay_month_amount
????else:
????????d?=?loan_amount?/?repay_month_amount
????????month_repay?=?[round(d?+?(loan_amount?-?d?*?(month?-?1))?*?month_interest_rate,?3)
???????????????????????for?month?in?range(1,?repay_month_amount?+?1)]
????fig?=?px.line(x=[f'第{i}月'?for?i?in?range(1,?repay_month_amount?+?1)],
??????????????????y=month_repay,
??????????????????title='每月還款金額變化曲線(總支出:{}元)'.format(round(sum(month_repay),?2)),
??????????????????template='plotly_white')
????return?fig
@app.callback(
????Output('repay_timeline',?'figure'),
????Input('start',?'n_clicks'),
????[State('loan_amount',?'value'),
?????State('repay_month_amount',?'value'),
?????State('interest_rate',?'value'),
?????State('repay_method',?'value')],
????prevent_initial_call=True
)
def?refresh_repay_timeline(n_clicks,?loan_amount,?repay_month_amount,?interest_rate,?repay_method):
????time.sleep(0.2)?#?增加應(yīng)用的動態(tài)效果
????return?make_line_graph(loan_amount,?repay_month_amount,?interest_rate,?repay_method)
if?__name__?==?'__main__':
????app.run_server(debug=True)
以上就是本文全部內(nèi)容,下一期中將為大家介紹Dash中更加巧妙的回調(diào)技巧,敬請期待。歡迎在評論區(qū)中與我進(jìn)行討論~

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