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_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
❞
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): 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
❞
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)