(数据科学学习手札105)Python+Dash快速web应用开发——回调交互篇(中)

本文示例代码已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes

  • 😋由我开源的先进Dash组件库feffery-antd-components正处于早期测试版本阶段,欢迎前往官网http://fac.feffery.tech/了解更多

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

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

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

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):
    # 此处故意不处理默认状态下输入值为None的情况
    return int(value) ** 2


@app.callback(
    # 此处故意写错为不存在的id
    Output('output3', 'children'),
    Input('input2', 'value')
)
def callback2(value):

    if value:
        return int(value) ** 0.5

if __name__ == "__main__":
    app.run_server(debug=True)
图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)
图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):
    try:

        return dcc.Dropdown(
            id='output_dropdown',
            options=[
                {'label': i, 'value': i}
                for i in range(int(value))
            ]
        )
    except ValueError:
        return dash.no_update


@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

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中更加巧妙的回调技巧,敬请期待。欢迎在评论区中与我进行讨论~

posted @ 2021-01-30 15:29  费弗里  阅读(5792)  评论(3编辑  收藏  举报