4

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

 3 years ago
source link: http://www.cnblogs.com/feffery/p/14349206.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

1 简介

这是我的系列教程 Python+Dash快速web应用开发 的第四期,在上一期的文章中,我们进入了 Dash 核心内容—— callback ,get到如何在不编写js代码的情况下,轻松实现前后端异步通信,为创造任意交互方式的 Dash 应用打下基础。

而在今天的文章中,我将带大家学习有关 Dash回调 的一些非常实用,且不算复杂的额外特性,让你更加熟悉 Dash 的回调交互~

Zn2iemI.png!mobile
图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)
uAVRfmj.gif!mobile
图2

可以看到,debug模式下,我们对源代码做出的修改在保存之后,都会受到 Dash 的监听,从而做出反馈(注意一定要在作出修改的代码完整之后再保存,否则代码写到一半就保存会引起语法错误等中断当前 Dash 实例)。

  • 对回调结构进行可视化

你可能已经注意到,在开启debug模式之后,我们浏览器中的 Dash 应用右下角出现的蓝色logo,点击打开折叠,可以看到几个按钮:

UzMz2un.png!mobile
图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)
naqUbyQ.gif!mobile
图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()
eqQfaav.gif!mobile
图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)
rmi67bA.gif!mobile
图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)
nymE3eu.gif!mobile
图7

可以看到,参数添加后, Dash 会自动忽略类似的回调匹配错误,非常的实用,这个知识点我们会在以后的 前后端分离 篇中频繁地使用到,所以一定要记住它。

3 编写一个贷款计算器

get完今天所学的知识点后,我们通过实际的例子,来巩固上一期及这一期的内容,帮助大家对 Dash 中的回调基础知识有更好的理解。

今天我们要编写的例子,是贷款计算器,要编写出一个实际的贷款计算器,我们需要组织以下用户输入内容:

  • 贷款总金额
  • 还款月份数量
  • 年利率
  • 还款方式

其中还款方式主要有 等额本息等额本金 两种,我们利用之前介绍过的 dash-bootstrap-components 来搭建页面,其中 贷款金额还款月份数量 以及 年利率 我们都使用 Input() 部件来实现,并利用参数 type="number" 来约束其类型为数值。

还款方式 是二选一,所以我们使用部件 RadioItems() 来实现,最后设置计算按钮,配合以前介绍过的 State()n_clicks 来交互执行计算,并以 plotly.express 折线图的形式呈现计算结果(这部分我们将在之后的 嵌入可视化 中详细介绍),最终得到的效果如下:

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK