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

費弗裡發表於2021-01-30

本文示例程式碼已上傳至我的Github倉庫https://github.com/CNFeffery/DataScienceStudyNotes

1 簡介

   這是我的系列教程Python+Dash快速web應用開發的第四期,在上一期的文章中,我們進入了Dash核心內容——callback,get到如何在不編寫js程式碼的情況下,輕鬆實現前後端非同步通訊,為創造任意互動方式的Dash應用打下基礎。

  而在今天的文章中,我將帶大家學習有關Dash回撥的一些非常實用,且不算複雜的額外特性,讓你更加熟悉Dash的回撥互動~

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

2 Dash中的回撥實用小特性

2.1 靈活使用debug模式

  開發階段,在Dash中使用run_server()啟動我們的應用時,可以新增引數debug=True來切換為debug模式,在這種模式下,我們可以獲得以下輔助功能:

  • 熱過載

  熱過載指的是,我們在編寫完一個Dash的完整應用並在debug模式下啟動之後,在保持應用執行的情況下,修改原始碼並儲存之後,瀏覽器中執行的Dash例項會自動重啟重新整理,就像下面的例子一樣:

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)
(資料科學學習手札105)Python+Dash快速web應用開發——回撥互動篇(中)
圖2

  可以看到,debug模式下,我們對原始碼做出的修改在儲存之後,都會受到Dash的監聽,從而做出反饋(注意一定要在作出修改的程式碼完整之後再儲存,否則程式碼寫到一半就儲存會引起語法錯誤等中斷當前Dash例項)。

  • 對回撥結構進行視覺化

  你可能已經注意到,在開啟debug模式之後,我們瀏覽器中的Dash應用右下角出現的藍色logo,點選開啟摺疊,可以看到幾個按鈕:

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

  其中第一個Callbacks非常有意思,它可以幫助我們對當前Dash應用中的回撥關係進行視覺化,譬如下面的例子:

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)
(資料科學學習手札105)Python+Dash快速web應用開發——回撥互動篇(中)
圖4

  可以看到,我們開啟Callbacks之後,可以看到每個回撥的輸入輸出、通訊延遲等資訊,可以幫助我們更有條理的組織各個回撥。

  • 展示執行錯誤資訊

  既然主要功能是debug,自然是可以幫助我們在程式出現錯誤時列印具體的錯誤資訊,我們在前面app2.py例子的基礎上,故意製造一些錯誤:

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預設為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()
(資料科學學習手札105)Python+Dash快速web應用開發——回撥互動篇(中)
圖5

  可以看到,我們故意製造出的兩種錯誤:不處理Input()預設的缺失值valueOutput()傳入不存在的id,都在瀏覽器中得到輸出,並且可自由檢視錯誤資訊,這對我們開發過程幫助很大。

2.2 阻止應用的初始回撥

  在前面的app3例子中,我們故意製造出的錯誤之一是不處理Input()預設的缺失值value,這裡的錯誤展開來說是因為Input()部件value屬性的預設值是None,使得剛載入應用還未輸入值時引發了回撥中計算部分的邏輯錯誤。

  類似這樣的情況很多,可以通過給部件相應屬性設定預設值或者在回撥中寫條件判斷等方式處理,就像app2中那樣,但如果這樣的部件比較多,一個一個逐一處理還是比較繁瑣,而Dash中提供了阻止初始回撥的特性,只需要在app.callback裝飾器中設定引數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)
(資料科學學習手札105)Python+Dash快速web應用開發——回撥互動篇(中)
圖6

  可以看到,設定完引數後,Dash應用被訪問時,不會自動執行首次回撥,非常的方便。

2.3 忽略回撥匹配錯誤

  在前面我們還製造出了Output()傳入不存在的id這種錯誤,也就是回撥函式查詢輸入輸出等關係時,出現匹配失敗的情況。

  但在很多時候,我們需要在發生某些互動回撥時,才建立返回一些具有指定id的部件,這時如果程式中提前寫好了針對這些初始化時不存在的部件的回撥,就會觸發前面的錯誤。

  在Dash中提供瞭解決此類問題的方法,在建立app例項時新增引數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部件共有{}個選項'.format(options.__len__())

if __name__ == "__main__":
    app.run_server(debug=True)
(資料科學學習手札105)Python+Dash快速web應用開發——回撥互動篇(中)
圖7

  可以看到,引數新增後,Dash會自動忽略類似的回撥匹配錯誤,非常的實用,這個知識點我們會在以後的前後端分離篇中頻繁地使用到,所以一定要記住它。

3 編寫一個貸款計算器

  get完今天所學的知識點後,我們通過實際的例子,來鞏固上一期及這一期的內容,幫助大家對Dash中的回撥基礎知識有更好的理解。

  今天我們要編寫的例子,是貸款計算器,要編寫出一個實際的貸款計算器,我們需要組織以下使用者輸入內容:

  • 貸款總金額
  • 還款月份數量
  • 年利率
  • 還款方式

  其中還款方式主要有等額本息等額本金兩種,我們利用之前介紹過的dash-bootstrap-components來搭建頁面,其中貸款金額還款月份數量以及年利率我們都使用Input()部件來實現,並利用引數type="number"來約束其型別為數值。

  而還款方式是二選一,所以我們使用部件RadioItems()來實現,最後設定計算按鈕,配合以前介紹過的State()n_clicks來互動執行計算,並以plotly.express折線圖的形式呈現計算結果(這部分我們將在之後的嵌入視覺化中詳細介紹),最終得到的效果如下:

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

  程式碼如下:

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("計劃還款月數", 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中更加巧妙的回撥技巧,敬請期待。歡迎在評論區中與我進行討論~

相關文章