1. 程式人生 > >(資料科學學習手札105)Python+Dash快速web應用開發——回撥互動篇(中)

(資料科學學習手札105)Python+Dash快速web應用開發——回撥互動篇(中)

> 本文示例程式碼已上傳至我的`Github`倉庫[https://github.com/CNFeffery/DataScienceStudyNotes](https://github.com/CNFeffery/DataScienceStudyNotes) # 1 簡介    這是我的系列教程**Python+Dash快速web應用開發**的第四期,在上一期的文章中,我們進入了`Dash`核心內容——`callback`,get到如何在不編寫js程式碼的情況下,輕鬆實現前後端非同步通訊,為創造任意互動方式的`Dash`應用打下基礎。   而在今天的文章中,我將帶大家學習有關`Dash`中**回撥**的一些非常實用,且不算複雜的額外特性,讓你更加熟悉`Dash`的回撥互動~
圖1
# 2 Dash中的回撥實用小特性 ## 2.1 靈活使用debug模式   開發階段,在`Dash`中使用`run_server()`啟動我們的應用時,可以新增引數`debug=True`來切換為**debug**模式,在這種模式下,我們可以獲得以下輔助功能: - **熱過載**   熱過載指的是,我們在編寫完一個`Dash`的完整應用並在debug模式下啟動之後,在保持應用執行的情況下,修改原始碼並儲存之後,瀏覽器中執行的`Dash`例項會自動重啟重新整理,就像下面的例子一樣: > app1.py ```Python 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) ```
圖2
  可以看到,debug模式下,我們對原始碼做出的修改在儲存之後,都會受到`Dash`的監聽,從而做出反饋(注意一定要在作出修改的程式碼完整之後再儲存,否則程式碼寫到一半就儲存會引起語法錯誤等中斷當前`Dash`例項)。 - **對回撥結構進行視覺化**   你可能已經注意到,在開啟debug模式之後,我們瀏覽器中的`Dash`應用右下角出現的藍色logo,點選開啟摺疊,可以看到幾個按鈕:
圖3
  其中第一個**Callbacks**非常有意思,它可以幫助我們對當前`Dash`應用中的回撥關係進行視覺化,譬如下面的例子: > app2.py ```Python 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) ```
圖4
  可以看到,我們開啟**Callbacks**之後,可以看到每個回撥的輸入輸出、通訊延遲等資訊,可以幫助我們更有條理的組織各個回撥。 - **展示執行錯誤資訊**   既然主要功能是debug,自然是可以幫助我們在程式出現錯誤時列印具體的錯誤資訊,我們在前面`app2.py`例子的基礎上,故意製造一些錯誤: > app3.py ```Python 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預設為False dbc.Container( [ dcc.Dropdown(), '測試', dcc.Dropdown() ] ), html.Hr(), # 水平分割線 # fluid設定為True dbc.Container( [ dcc.Dropdown(), '測試', dcc.Dropdown() ], fluid=True ) ] ) if __name__ == "__main__": app.run_server() ```
圖5
  可以看到,我們故意製造出的兩種錯誤:**不處理Input()預設的缺失值value**、**Output()傳入不存在的id**,都在瀏覽器中得到輸出,並且可自由檢視錯誤資訊,這對我們開發過程幫助很大。 ## 2.2 阻止應用的初始回撥   在前面的`app3`例子中,我們故意製造出的錯誤之一是**不處理Input()預設的缺失值value**,這裡的錯誤展開來說是因為`Input()`部件`value`屬性的預設值是None,使得剛載入應用還未輸入值時引發了回撥中計算部分的邏輯錯誤。   類似這樣的情況很多,可以通過給部件相應屬性設定預設值或者在回撥中寫條件判斷等方式處理,就像`app2`中那樣,但如果這樣的部件比較多,一個一個逐一處理還是比較繁瑣,而`Dash`中提供了**阻止初始回撥**的特性,只需要在`app.callback`裝飾器中設定引數`prevent_initial_call=True`即可: > app4.py ```Python 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) ```
圖6
  可以看到,設定完引數後,`Dash`應用被訪問時,不會自動執行首次回撥,非常的方便。 ## 2.3 忽略回撥匹配錯誤   在前面我們還製造出了**Output()傳入不存在的id**這種錯誤,也就是回撥函式查詢輸入輸出等關係時,出現匹配失敗的情況。   但在很多時候,我們需要在發生某些互動回撥時,才建立返回一些具有指定**id**的部件,這時如果程式中提前寫好了針對這些初始化時**不存在**的部件的回撥,就會觸發前面的錯誤。   在`Dash`中提供瞭解決此類問題的方法,在建立`app`例項時新增引數`suppress_callback_exceptions=True`即可: > app5.py ```Python 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部件共有{}個選項'.format(options.__len__()) if __name__ == "__main__": app.run_server(debug=True) ```
圖7
  可以看到,引數新增後,`Dash`會自動忽略類似的回撥匹配錯誤,非常的實用,這個知識點我們會在以後的**前後端分離**篇中頻繁地使用到,所以一定要記住它。 # 3 編寫一個貸款計算器   get完今天所學的知識點後,我們通過實際的例子,來鞏固上一期及這一期的內容,幫助大家對`Dash`中的回撥基礎知識有更好的理解。   今天我們要編寫的例子,是貸款計算器,要編寫出一個實際的貸款計算器,我們需要組織以下使用者輸入內容: - **貸款總金額** - **還款月份數量** - **年利率** - **還款方式**   其中還款方式主要有**等額本息**與**等額本金**兩種,我們利用之前介紹過的`dash-bootstrap-components`來搭建頁面,其中**貸款金額**、**還款月份數量**以及**年利率**我們都使用`Input()`部件來實現,並利用引數`type="number"`來約束其型別為數值。   而**還款方式**是二選一,所以我們使用部件**RadioItems()**來實現,最後設定計算按鈕,配合以前介紹過的`State()`和`n_clicks`來互動執行計算,並以`plotly.express`折線圖的形式呈現計算結果(這部分我們將在之後的**嵌入視覺化**中詳細介紹),最終得到的效果如下:
圖8
  程式碼如下: > app6.py ```Python 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("計劃還款月數", addon_type="prepend"), dbc.Input( id='repay_month_amount', placeholder='請輸入計劃還款月數', 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('開始計算', 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) # 增加應用的動態效果 return make_line_graph(loan_amount, repay_month_amount, interest_rate, repay_method) if __name__ == '__main__': app.run_server(debug=True) ``` ---   以上就是本文全部內容,下一期中將為大家介紹`Dash`中更加巧妙的回撥技巧,敬請期待。歡迎在評論區中與我進