飞道的博客

Python+Dash快速web应用开发:回调交互篇(中)

561人阅读  评论(0)


添加微信号"CNFeffery"加入技术交流群

本文示例代码已上传至我的Github仓库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


   
  1. import dash
  2. import dash_html_components as html
  3. app = dash.Dash(__name__)
  4. app.layout = html.Div(
  5.     html.H1( '我是热重载之前!')
  6. )
  7. if __name__ ==  '__main__':
  8.     app.run_server(debug=True)
图2

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

  • 「对回调结构进行可视化」

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

图3

其中第一个「Callbacks」非常有意思,它可以帮助我们对当前Dash应用中的回调关系进行可视化,譬如下面的例子:

app2.py


   
  1. import dash
  2. import dash_bootstrap_components as dbc
  3. import dash_html_components as html
  4. from dash.dependencies  import Input, Output
  5. app = dash.Dash(
  6.     __name__,
  7.     external_stylesheets=[ 'css/bootstrap.min.css']
  8. )
  9. app.layout = html.Div(
  10.     dbc.Container(
  11.         [
  12.             html.Br(),
  13.             html.Br(),
  14.             html.Br(),
  15.             dbc.Row(
  16.                 [
  17.                     dbc.Col(
  18.                         dbc.Input(id= 'input1'),
  19.                         width= 4
  20.                     ),
  21.                     dbc.Col(
  22.                         dbc.Label(id= 'output1'),
  23.                         width= 4
  24.                     )
  25.                 ]
  26.             ),
  27.             dbc.Row(
  28.                 [
  29.                     dbc.Col(
  30.                         dbc.Input(id= 'input2'),
  31.                         width= 4
  32.                     ),
  33.                     dbc.Col(
  34.                         dbc.Label(id= 'output2'),
  35.                         width= 4
  36.                     )
  37.                 ]
  38.             )
  39.         ]
  40.     )
  41. )
  42. @app.callback(
  43.     Output( 'output1''children'),
  44.     Input( 'input1''value')
  45. )
  46. def callback1(value):
  47.      if value:
  48.          return  int(value) **  2
  49. @app.callback(
  50.     Output( 'output2''children'),
  51.     Input( 'input2''value')
  52. )
  53. def callback2(value):
  54.      if value:
  55.          return  int(value) **  0.5
  56. if __name__ ==  "__main__":
  57.     app.run_server(debug=True)
图4

可以看到,我们打开「Callbacks」之后,可以看到每个回调的输入输出、通信延迟等信息,可以帮助我们更有条理的组织各个回调。

  • 「展示运行错误信息」

既然主要功能是debug,自然是可以帮助我们在程序出现错误时打印具体的错误信息,我们在前面app2.py例子的基础上,故意制造一些错误:

app3.py


   
  1. import dash
  2. import dash_bootstrap_components as dbc
  3. import dash_core_components as dcc
  4. import dash_html_components as html
  5. app = dash.Dash(
  6.     __name__,
  7.     external_stylesheets=[ 'css/bootstrap.min.css']
  8. )
  9. app.layout = html.Div(
  10.     [
  11.         # fluid默认为False
  12.         dbc.Container(
  13.             [
  14.                 dcc.Dropdown(),
  15.                  '测试',
  16.                 dcc.Dropdown()
  17.             ]
  18.         ),
  19.         html.Hr(), # 水平分割线
  20.         # fluid设置为True
  21.         dbc.Container(
  22.             [
  23.                 dcc.Dropdown(),
  24.                  '测试',
  25.                 dcc.Dropdown()
  26.             ],
  27.             fluid=True
  28.         )
  29.     ]
  30. )
  31. if __name__ ==  "__main__":
  32.     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


   
  1. import dash
  2. import dash_bootstrap_components as dbc
  3. import dash_html_components as html
  4. from dash.dependencies  import Input, Output
  5. app = dash.Dash(
  6.     __name__,
  7.     external_stylesheets=[ 'css/bootstrap.min.css']
  8. )
  9. app.layout = html.Div(
  10.     dbc.Container(
  11.         [
  12.             html.Br(),
  13.             html.Br(),
  14.             html.Br(),
  15.             dbc.Row(
  16.                 [
  17.                     dbc.Col(
  18.                         dbc.Input(id= 'input1'),
  19.                         width= 4
  20.                     ),
  21.                     dbc.Col(
  22.                         dbc.Label(id= 'output1'),
  23.                         width= 4
  24.                     )
  25.                 ]
  26.             )
  27.         ]
  28.     )
  29. )
  30. @app.callback(
  31.     Output( 'output1''children'),
  32.     Input( 'input1''value'),
  33.     prevent_initial_call=True
  34. )
  35. def callback1(value):
  36.      return  int(value) **  2
  37. if __name__ ==  "__main__":
  38.     app.run_server(debug=True)
图6

可以看到,设置完参数后,Dash应用被访问时,不会自动执行首次回调,非常的方便。

2.3 忽略回调匹配错误

在前面我们还制造出了「Output()传入不存在的id」这种错误,也就是回调函数查找输入输出等关系时,出现匹配失败的情况。

但在很多时候,我们需要在发生某些交互回调时,才创建返回一些具有指定「id」的部件,这时如果程序中提前写好了针对这些初始化时「不存在」的部件的回调,就会触发前面的错误。

Dash中提供了解决此类问题的方法,在创建app实例时添加参数suppress_callback_exceptions=True即可:

app5.py


   
  1. import dash
  2. import dash_bootstrap_components as dbc
  3. import dash_html_components as html
  4. import dash_core_components as dcc
  5. from dash.dependencies  import Input, Output
  6. app = dash.Dash(
  7.     __name__,
  8.     external_stylesheets=[ 'css/bootstrap.min.css'],
  9.     # suppress_callback_exceptions=True
  10. )
  11. app.layout = html.Div(
  12.     dbc.Container(
  13.         [
  14.             dbc.Row(
  15.                 [
  16.                     dbc.Col(
  17.                         dbc.Input(id= 'input_num')
  18.                     ),
  19.                     dbc.Col(id= 'output_item')
  20.                 ]
  21.             ),
  22.             dbc.Row(
  23.                 dbc.Col(
  24.                     dbc.Label(id= 'output_desc')
  25.                 )
  26.             )
  27.         ]
  28.     )
  29. )
  30. @app.callback(
  31.     Output( 'output_item''children'),
  32.     Input( 'input_num''value'),
  33.     prevent_initial_call=True
  34. )
  35. def callback1(value):
  36.      return dcc.Dropdown(
  37.         id= 'output_dropdown',
  38.         options=[
  39.             { 'label': i,  'value': i}
  40.              for i in  range( int(value))
  41.         ]
  42.     )
  43. @app.callback(
  44.     Output( 'output_desc''children'),
  45.     Input( 'output_dropdown''options'),
  46.     prevent_initial_call=True
  47. )
  48. def callback2(options):
  49.      return  '生成的Dropdown部件共有{}个选项'.format(options.__len__())
  50. if __name__ ==  "__main__":
  51.     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


   
  1. import dash
  2. import dash_html_components as html
  3. import plotly.express as px
  4. import dash_core_components as dcc
  5. import dash_bootstrap_components as dbc
  6. from dash.dependencies  import Output, Input, State
  7. import time
  8. app = dash.Dash(
  9.     __name__,
  10.     external_stylesheets=[ 'css/bootstrap.min.css'],
  11.     suppress_callback_exceptions=True
  12. )
  13. app.layout = html.Div(
  14.     dbc.Container(
  15.         [
  16.             html.Br(),
  17.             html.Br(),
  18.             html.Br(),
  19.             html.Br(),
  20.             dbc.Row(
  21.                 dbc.Col(
  22.                     dbc.InputGroup(
  23.                         [
  24.                             dbc.InputGroupAddon( "贷款金额", addon_type= "prepend"),
  25.                             dbc.Input(
  26.                                 id= 'loan_amount',
  27.                                 placeholder= '请输入贷款总金额',
  28.                                  type= "number",
  29.                                 value= 100
  30.                             ),
  31.                             dbc.InputGroupAddon( "万元", addon_type= "append"),
  32.                         ],
  33.                     ),
  34.                     width={ 'size'6'offset'3}
  35.                 )
  36.             ),
  37.             html.Br(),
  38.             dbc.Row(
  39.                 dbc.Col(
  40.                     dbc.InputGroup(
  41.                         [
  42.                             dbc.InputGroupAddon( "计划还款月数", addon_type= "prepend"),
  43.                             dbc.Input(
  44.                                 id= 'repay_month_amount',
  45.                                 placeholder= '请输入计划还款月数',
  46.                                  type= "number",
  47.                                 value= 24,
  48.                                 min= 1,
  49.                                 step= 1
  50.                             ),
  51.                             dbc.InputGroupAddon( "个月", addon_type= "append"),
  52.                         ],
  53.                     ),
  54.                     width={ 'size'6'offset'3}
  55.                 )
  56.             ),
  57.             html.Br(),
  58.             dbc.Row(
  59.                 dbc.Col(
  60.                     dbc.InputGroup(
  61.                         [
  62.                             dbc.InputGroupAddon( "年利率", addon_type= "prepend"),
  63.                             dbc.Input(
  64.                                 id= 'interest_rate',
  65.                                 placeholder= '请输入年利率',
  66.                                  type= "number",
  67.                                 value= 5,
  68.                                 min= 0,
  69.                                 step= 0.001
  70.                             ),
  71.                             dbc.InputGroupAddon( "%", addon_type= "append"),
  72.                         ],
  73.                     ),
  74.                     width={ 'size'6'offset'3}
  75.                 )
  76.             ),
  77.             html.Br(),
  78.             dbc.Row(
  79.                 dbc.Col(
  80.                     dbc.RadioItems(
  81.                         id= "repay_method",
  82.                         options=[
  83.                             { "label""等额本息""value""等额本息"},
  84.                             { "label""等额本金""value""等额本金"}
  85.                         ],
  86.                         value= '等额本息'
  87.                     ),
  88.                     width={ 'size'6'offset'3}
  89.                 ),
  90.             ),
  91.             html.Br(),
  92.             dbc.Row(
  93.                 dbc.Col(
  94.                     dbc.Button( '开始计算', id= 'start', n_clicks= 0, color= 'light'),
  95.                     width={ 'size'6'offset'3}
  96.                 ),
  97.             ),
  98.             html.Br(),
  99.             dbc.Row(
  100.                 dbc.Col(
  101.                     dcc.Loading(dcc.Graph(id= 'repay_timeline')),
  102.                     width={ 'size'6'offset'3}
  103.                 ),
  104.             ),
  105.         ],
  106.         fluid=True
  107.     )
  108. )
  109. def make_line_graph(loan_amount,
  110.                     repay_month_amount,
  111.                     interest_rate,
  112.                     repay_method):
  113.     interest_rate /=  100
  114.     loan_amount *=  10000
  115.     month_interest_rate = interest_rate /  12
  116.      if repay_method ==  '等额本息':
  117.         month_repay = loan_amount * month_interest_rate * pow(( 1 + month_interest_rate), repay_month_amount) / \
  118.                       (pow(( 1 + month_interest_rate), repay_month_amount) -  1)
  119.         month_repay = round(month_repay,  2)
  120.         month_repay = [month_repay] * repay_month_amount
  121.      else:
  122.         d = loan_amount / repay_month_amount
  123.         month_repay = [round(d + (loan_amount - d * (month -  1)) * month_interest_rate,  3)
  124.                         for month in  range( 1, repay_month_amount +  1)]
  125.     fig = px.line(x=[f '第{i}月'  for i in  range( 1, repay_month_amount +  1)],
  126.                   y=month_repay,
  127.                   title= '每月还款金额变化曲线(总支出:{}元)'.format(round(sum(month_repay),  2)),
  128.                   template= 'plotly_white')
  129.      return fig
  130. @app.callback(
  131.     Output( 'repay_timeline''figure'),
  132.     Input( 'start''n_clicks'),
  133.     [State( 'loan_amount''value'),
  134.      State( 'repay_month_amount''value'),
  135.      State( 'interest_rate''value'),
  136.      State( 'repay_method''value')],
  137.     prevent_initial_call=True
  138. )
  139. def refresh_repay_timeline(n_clicks, loan_amount, repay_month_amount, interest_rate, repay_method):
  140.     time.sleep( 0.2) # 增加应用的动态效果
  141.      return make_line_graph(loan_amount, repay_month_amount, interest_rate, repay_method)
  142. if __name__ ==  '__main__':
  143.     app.run_server(debug=True)

以上就是本文全部内容,下一期中将为大家介绍Dash中更加巧妙的回调技巧,敬请期待。欢迎在评论区中与我进行讨论~

加入知识星球【我们谈论数据科学】

300+小伙伴一起学习!




· 推荐阅读 ·

在模仿中精进数据可视化07:星球研究所大坝分布可视化

Python+Dash快速web应用开发:回调交互篇(上)

这个Pandas函数可以自动爬取Web图表


转载:https://blog.csdn.net/baoqiangwang/article/details/113449526
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场