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

1 简介

   这是我的系列教程Python+Dash快速web应用开发的第五期,在上一期的文章中,我们针对Dash中有关回调的一些技巧性的特性进行了介绍,使得我们可以更愉快地为Dash应用编写回调交互功能。

  而今天的文章作为回调交互系统性内容的最后一期,我将带大家get一些Dash中实际应用效果惊人的高级回调特性,系好安全带,我们起飞~

图1

2 Dash中的高级回调特性

2.1 控制部分回调输出不更新

  在很多应用场景下,我们给某个回调函数绑定了多个Output(),这时如果这些Output()并不是每次触发回调都需要被更新,那么就可以根据Input()值的不同,来配合dash.no_update作为对应Output()的返回值,从而实现部分Output()不更新,譬如下面的例子:

app1.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. import time
  6. app = dash.Dash(__name__)
  7. app.layout = html.Div(
  8. dbc.Container(
  9. [
  10. html.Br(),
  11. html.Br(),
  12. html.Br(),
  13. dbc.Row(
  14. dbc.Col(
  15. dbc.Button('按钮',
  16. color='primary',
  17. id='button',
  18. n_clicks=0)
  19. )
  20. ),
  21. html.Br(),
  22. dbc.Row(
  23. [
  24. dbc.Col('尚未触发', id='record-1'),
  25. dbc.Col('尚未触发', id='record-2'),
  26. dbc.Col('尚未触发', id='record-n')
  27. ]
  28. )
  29. ]
  30. )
  31. )
  32. @app.callback(
  33. [Output('record-1', 'children'),
  34. Output('record-2', 'children'),
  35. Output('record-n', 'children'),
  36. ],
  37. Input('button', 'n_clicks'),
  38. prevent_initial_call=True
  39. )
  40. def record_click_event(n_clicks):
  41. if n_clicks == 1:
  42. return (
  43. '第1次点击:{}'.format(time.strftime('%H:%M:%S', time.localtime(time.time()))),
  44. dash.no_update,
  45. dash.no_update
  46. )
  47. elif n_clicks == 2:
  48. return (
  49. dash.no_update,
  50. '第2次点击:{}'.format(time.strftime('%H:%M:%S', time.localtime(time.time()))),
  51. dash.no_update
  52. )
  53. elif n_clicks >= 3:
  54. return (
  55. dash.no_update,
  56. dash.no_update,
  57. '第3次及以上点击:{}'.format(time.strftime('%H:%M:%S', time.localtime(time.time()))),
  58. )
  59. if __name__ == '__main__':
  60. app.run_server(debug=True)

图2

  可以观察到,我们根据n_clicks数值的不同,在对应各个Output()返回值中对符合条件的部件进行更新,其他的都用dash.no_update来代替,从而实现了局部更新,非常实用且简单。

2.2 基于模式匹配的回调

  这是Dash在1.11.0版本开始引入的新特性,它所实现的功能是将多个部件绑定组织在同一个id属性下,这听起来有一点抽象,我们先从一个形象的例子来出发:

  假如我们要开发一个简单的记账应用,它通过第一排若干Input()部件及一个Button()部件来记录并提交每笔账对应的相关信息,并且在最下方输出已记录账目金额之和:

app2.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. from dash.dependencies import Input, Output, State, ALL
  6. import re
  7. app = dash.Dash(__name__)
  8. app.layout = html.Div(
  9. [
  10. html.Br(),
  11. html.Br(),
  12. dbc.Container(
  13. dbc.Row(
  14. [
  15. dbc.Col(
  16. dbc.InputGroup(
  17. [
  18. dbc.InputGroupAddon("金额", addon_type="prepend"),
  19. dbc.Input(
  20. id='account-amount',
  21. placeholder='请输入金额',
  22. type="number",
  23. ),
  24. dbc.InputGroupAddon("元", addon_type="append"),
  25. ],
  26. ),
  27. width=5
  28. ),
  29. dbc.Col(
  30. dcc.Dropdown(
  31. id='account-type',
  32. options=[
  33. {'label': '生活开销', 'value': '生活开销'},
  34. {'label': '人情往来', 'value': '人情往来'},
  35. {'label': '医疗保健', 'value': '医疗保健'},
  36. {'label': '旅游休闲', 'value': '旅游休闲'},
  37. ],
  38. placeholder='请选择类型:'
  39. ),
  40. width=5
  41. ),
  42. dbc.Col(
  43. dbc.Button('提交记录', id='account-submit'),
  44. width=2
  45. )
  46. ]
  47. )
  48. ),
  49. html.Br(),
  50. dbc.Container([], id='account-record-container'),
  51. dbc.Container('暂无记录!', id='account-record-sum')
  52. ]
  53. )
  54. @app.callback(
  55. Output('account-record-container', 'children'),
  56. Input('account-submit', 'n_clicks'),
  57. [State('account-record-container', 'children'),
  58. State('account-amount', 'value'),
  59. State('account-type', 'value')],
  60. prevent_initial_call=True
  61. )
  62. def update_account_records(n_clicks, children, account_amount, account_type):
  63. '''
  64. 用于处理每一次的记账输入并渲染前端记录
  65. '''
  66. if account_amount and account_type:
  67. children.append(dbc.Row(
  68. dbc.Col(
  69. '【{}】类开销【{}】元'.format(account_type, account_amount)
  70. ),
  71. # 以字典形式定义id
  72. id={'type': 'single-account_record', 'index': children.__len__()}
  73. ))
  74. return children
  75. @app.callback(
  76. Output('account-record-sum', 'children'),
  77. Input({'type': 'single-account_record', 'index': ALL}, 'children'),
  78. prevent_initial_call=True
  79. )
  80. def refresh_account_sum(children):
  81. '''
  82. 对多部件集合single-account_record下所有账目记录进行求和
  83. '''
  84. return '账本总开销:{}'.format(sum([int(re.findall('\d+',
  85. child['props']['children'])[0])
  86. for child in children]))
  87. if __name__ == '__main__':
  88. app.run_server(debug=True)

图3

  上面这个应用中,体现出的模式匹配内容即为开头从dash.dependencies引入的ALL,它是Dash模式匹配中的一种模式,而我们在回调函数update_account_records()中为已有记账记录追加新纪录时,使用到:

  1. # 以字典形式定义id
  2. id={'type': 'single-account_record', 'index': children.__len__()}

  这里不同于以前我们采取的id=某个字符串的定义方法,换成字典之后,其type键值对用来记录唯一id信息,每一次新纪录追加时type值都相等,因为它们被组织为同id部件集合,而键值对index则用于在type值相同的一个部件集合下,区分出不同的独立部件元素。

  因为将传统的唯一id部件替换成同id部件集合,所以我们后面的回调函数refresh_account_sum()的输入元素只需要定义单个Input()即可,再在函数内部按照不同的index值取出需要的集合内各成员记录值,非常便于我们书写出简练清爽的Dash代码,便于之后进一步的修改与重构。

  你可以通过最下面打印出的每次refresh_account_sum()所接收到的children参数json格式结果来弄清我是如何在return值的地方取出历史记账金额并计算的。

  而除了上面介绍的一股脑返回所有集合内成员部件的ALL模式之外,还有另一种更有针对性的MATCH模式,它应用于结合内成员部件可交互输入值的情况,譬如下面这个简单的例子,我们定义一个简单的用于查询省份行政代码的应用,配合MATCH模式来实现彼此成对独立输出:

app3.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, State, MATCH
  5. import dash_core_components as dcc
  6. app = dash.Dash(__name__)
  7. app.layout = html.Div(
  8. [
  9. html.Br(),
  10. html.Br(),
  11. html.Br(),
  12. dbc.Container(
  13. [
  14. dbc.Row(
  15. dbc.Col(
  16. dbc.Button('新增查询', id='add-item', outline=True)
  17. )
  18. ),
  19. html.Hr()
  20. ]
  21. ),
  22. dbc.Container([], id='query-container')
  23. ]
  24. )
  25. region2code = {
  26. '北京市': '110000000000',
  27. '重庆市': '500000000000',
  28. '安徽省': '340000000000'
  29. }
  30. @app.callback(
  31. Output('query-container', 'children'),
  32. Input('add-item', 'n_clicks'),
  33. State('query-container', 'children'),
  34. prevent_initial_call=True
  35. )
  36. def add_query_item(n_clicks, children):
  37. children.append(
  38. dbc.Row(
  39. [
  40. dbc.Col(
  41. [
  42. # 生成index相同的dropdown部件与文字输出部件
  43. dcc.Dropdown(id={'type': 'select-province', 'index': children.__len__()},
  44. options=[{'label': label, 'value': label} for label in region2code.keys()],
  45. placeholder='选择省份:'),
  46. html.P('请输入要查询的省份!', id={'type': 'code-output', 'index': children.__len__()})
  47. ]
  48. )
  49. ]
  50. )
  51. )
  52. return children
  53. @app.callback(
  54. Output({'type': 'code-output', 'index': MATCH}, 'children'),
  55. Input({'type': 'select-province', 'index': MATCH}, 'value')
  56. )
  57. def refresh_code_output(value):
  58. if value:
  59. return region2code[value]
  60. else:
  61. return dash.no_update
  62. if __name__ == '__main__':
  63. app.run_server(debug=True)

图4

  可以看到,在refresh_code_output()前应用MATCH模式匹配后,我们点击某个部件时,只有跟它index匹配的部件才会打印出相对应的输出,非常的方便~

2.3 多输入情况下获取部件触发情况

  在很多应用场景下,我们的某个回调可能拥有多个Input输入,但学过前面的内容我们已经清楚,不管有几个Input,只要其中有一个部件其输入属性发生变化,都会触发本轮回调,但是如果我们就想知道究竟是哪个Input触发了本轮回调该怎么办呢?

  这在Dash中可以通过dash.callback_context来方便的实现,它只能在回调函数中被执行,从而获取回调过程的诸多上下文信息,先从下面这个简单的例子出发看看dash.callback_context到底给我们带来了哪些有价值的信息:

app4.py

  1. import dash
  2. import dash_html_components as html
  3. import dash_bootstrap_components as dbc
  4. from dash.dependencies import Input, Output
  5. import json
  6. app = dash.Dash(__name__)
  7. app.layout = html.Div(
  8. dbc.Container(
  9. [
  10. html.Br(),
  11. html.Br(),
  12. html.Br(),
  13. dbc.Row(
  14. [
  15. dbc.Col(dbc.Button('A', id='A', n_clicks=0)),
  16. dbc.Col(dbc.Button('B', id='B', n_clicks=0)),
  17. dbc.Col(dbc.Button('C', id='C', n_clicks=0))
  18. ]
  19. ),
  20. dbc.Row(
  21. [
  22. dbc.Col(html.P('按钮A未点击', id='A-output')),
  23. dbc.Col(html.P('按钮B未点击', id='B-output')),
  24. dbc.Col(html.P('按钮C未点击', id='C-output'))
  25. ]
  26. ),
  27. dbc.Row(
  28. dbc.Col(
  29. html.Pre(id='raw-json')
  30. )
  31. )
  32. ]
  33. )
  34. )
  35. @app.callback(
  36. [Output('A-output', 'children'),
  37. Output('B-output', 'children'),
  38. Output('C-output', 'children'),
  39. Output('raw-json', 'children')],
  40. [Input('A', 'n_clicks'),
  41. Input('B', 'n_clicks'),
  42. Input('C', 'n_clicks')],
  43. prevent_initial_call=True
  44. )
  45. def refresh_output(A_n_clicks, B_n_clicks, C_n_clicks):
  46. # 获取本轮回调状态下的上下文信息
  47. ctx = dash.callback_context
  48. # 取出对应State、最近一次触发部件以及Input信息
  49. ctx_msg = json.dumps({
  50. 'states': ctx.states,
  51. 'triggered': ctx.triggered,
  52. 'inputs': ctx.inputs
  53. }, indent=2)
  54. return A_n_clicks, B_n_clicks, C_n_clicks, ctx_msg
  55. if __name__ == '__main__':
  56. app.run_server(debug=True)

图5

  可以看到,我们安插在回调函数里的dash.callback_context帮我们记录了从访问Dash开始,到最近一次执行回调期间,对应回调的输入输出信息变化情况、最近一次触发信息,非常的实用,可以支撑起很多复杂应用场景。

2.4 在浏览器端执行回调过程

  Dash虽然很方便,使得我们可以完全不用书写js代码就可以实现各种回调交互,但把所有的交互响应计算过程都交给服务端来做,省事倒是很省事,但会给服务器带来不小的计算和网络传输压力。

  因此很多容易频繁触发且与主要的数值计算无关的交互行为,完全可以搬到浏览器端执行,既快速又不吃服务器的计算资源,这也是当初JavaScript被发明的一个重要原因,而在Dash中,也为略懂js的用户提供了在浏览器端执行一些回调的贴心功能。

  从一个很简单的点击按钮,实现部分网页内容的打开与关闭出发,这里我们提前使用到dbc.Collapse部件,用于将所包含的网页内容与其它按钮部件的点击行为进行绑定:

app5.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, State
  5. app = dash.Dash(__name__)
  6. app.layout = html.Div(
  7. dbc.Container(
  8. [
  9. html.Br(),
  10. html.Br(),
  11. html.Br(),
  12. dbc.Button('服务端回调', id='server-button'),
  13. dbc.Collapse('服务端折叠内容', id='server-collapse'),
  14. html.Hr(),
  15. dbc.Button('浏览器端回调', id='browser-button'),
  16. dbc.Collapse('浏览器端折叠内容', id='browser-collapse'),
  17. ]
  18. )
  19. )
  20. @app.callback(
  21. Output('server-collapse', 'is_open'),
  22. Input('server-button', 'n_clicks'),
  23. State('server-collapse', 'is_open'),
  24. prevent_initial_call=True
  25. )
  26. def server_callback(n_clicks, is_open):
  27. return not is_open
  28. # 在dash中定义浏览器端回调函数的特殊格式
  29. app.clientside_callback(
  30. """
  31. function(n_clicks, is_open) {
  32. return !is_open;
  33. }
  34. """,
  35. Output('browser-collapse', 'is_open'),
  36. Input('browser-button', 'n_clicks'),
  37. State('browser-collapse', 'is_open'),
  38. prevent_initial_call=True
  39. )
  40. if __name__ == '__main__':
  41. app.run_server(debug=True)

  可以看到,服务端回调我们照常写,而浏览器端回调通过传入一个非常简单的js函数,在每次回调时接受输入并输出is_open的逻辑反值,从而实现了折叠内容的打开与关闭切换:

  1. function(n_clicks, is_open) {
  2. return !is_open;
  3. }

  便实现了浏览器端回调!

图6

  而如果你想要执行的浏览器端js回调函数代码有点长,还可以按照下图格式,把你的大段js回调函数代码放置于assets目录下对应路径里的js脚本中:

图7

  接着再在dash中按照下列格式编写关联输入输出与上述js回调的简短语句即可:

  1. app.clientside_callback(
  2. ClientsideFunction(
  3. namespace='命名空间名称',
  4. function_name='对应js回调函数名'
  5. ),
  6. '''
  7. 按顺序组织你的Output、Input以及State... ...
  8. '''
  9. )

  下面我们直接以大家喜闻乐见的数据可视化顶级框架echarts为例,来写一个根据不同输入值切换渲染出的图表类型,注意请从官网把依赖的echarts.min.js下载到我们的assets路径下对应位置,它会在我们的Dash应用启动时与所有assets下的资源一起自动被载入到浏览器中:

app6.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, ClientsideFunction
  6. app = dash.Dash(__name__)
  7. # 编写一个根据dropdown不同输入值切换对应图表类型的小应用
  8. app.layout = html.Div(
  9. dbc.Container(
  10. [
  11. html.Br(),
  12. dbc.Row(
  13. dbc.Col(
  14. dcc.Dropdown(
  15. id='chart-type',
  16. options=[
  17. {'label': '折线图', 'value': '折线图'},
  18. {'label': '堆积面积图', 'value': '堆积面积图'},
  19. ],
  20. value='折线图'
  21. ),
  22. width=3
  23. )
  24. ),
  25. html.Br(),
  26. dbc.Row(
  27. dbc.Col(
  28. html.Div(
  29. html.Div(
  30. id='main',
  31. style={
  32. 'height': '100%',
  33. 'width': '100%'
  34. }
  35. ),
  36. style={
  37. 'width': '800px',
  38. 'height': '500px'
  39. }
  40. )
  41. )
  42. )
  43. ]
  44. )
  45. )
  46. app.clientside_callback(
  47. # 关联自编js脚本中的相应回调函数
  48. ClientsideFunction(
  49. namespace='clientside',
  50. function_name='switch_chart'
  51. ),
  52. Output('main', 'children'),
  53. Input('chart-type', 'value')
  54. )
  55. if __name__ == '__main__':
  56. app.run_server(debug=True)

图8

  效果十分惊人,从此我们使用Dash不仅仅可以使用Python生态的工具,还可以配合对前端内容支持更好的js,起飞!


  至此我们的Dash回调交互三部曲已结束,接下来的文章我将开始带大家遨游丰富的各种Dash前端部件,涵盖了网页部件、数据可视化图表以及地图可视化等内容,敬请期待这场奇妙之旅吧~

  以上就是本文的全部内容,欢迎在评论区与我进行讨论。

(数据科学学习手札106)Python+Dash快速web应用开发——回调交互篇(下)的更多相关文章

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

    本文示例代码已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 这是我的系列教程Python+Dash快速web ...

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

    本文示例代码已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 这是我的系列教程Python+Dash快速web ...

  3. (数据科学学习手札102)Python+Dash快速web应用开发——基础概念篇

    本文示例代码与数据已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 这是我的新系列教程Python+Dash快 ...

  4. (数据科学学习手札108)Python+Dash快速web应用开发——静态部件篇(上)

    本文示例代码已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 这是我的系列教程Python+Dash快速web ...

  5. (数据科学学习手札109)Python+Dash快速web应用开发——静态部件篇(中)

    本文示例代码已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 这是我的系列教程Python+Dash快速web ...

  6. (数据科学学习手札118)Python+Dash快速web应用开发——特殊部件篇

    本文示例代码已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 这是我的系列教程Python+Dash快速web ...

  7. (数据科学学习手札103)Python+Dash快速web应用开发——页面布局篇

    本文示例代码已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 这是我的系列教程Python+Dash快速web ...

  8. (数据科学学习手札110)Python+Dash快速web应用开发——静态部件篇(下)

    本文示例代码已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 这是我的系列教程Python+Dash快速web ...

  9. (数据科学学习手札123)Python+Dash快速web应用开发——部署发布篇

    1 简介 这是我的系列教程Python+Dash快速web应用开发的第二十期,在上一期中我介绍了利用内网穿透的方式,将任何可以联网的电脑作为"服务器"向外临时发布你的Dash应用. ...

随机推荐

  1. 基于Python开发数据宽表实例

    搭建宽表作用,就是为了让业务部门的数据分析人员,在日常工作可以直接提取所需指标,快速做出对应专题的数据分析.在实际工作中,数据量及数据源繁多,如果每个数据分析人员都从计算加工到出报告,除了工作效率巨慢 ...

  2. 关于QTableWidget中单元格拖拽实现

    无重写函数实现单元格拖拽 缺点:需要额外设置一个记录拖拽起始行的私有成员变量和拖拽列的初始QList数据成员. 优点:无需重构函数,对于QT中信号和槽的灵活运用 信号和槽 // signal void ...

  3. 【ORA】ORA-27125:unable to create shared memory segment

    在安装Oracle 10g的时候出现一个了错误,在网上总结了一下大牛写的文章 ORA-27125:unable to create shared memory segment 安装时出现这个错误安装会 ...

  4. Android 中使用 config.gradle

    各位同学大家好,当然了如果不是同学,那么大家也同好.哈哈. 大家知道config.gradle 是什么吗?我也不知道.开个完笑,其实config.gradle 就是我们为了统一gradle 中的各种配 ...

  5. Nginx的简介和使用nginx实现请求转发

    一.什么是Nginx Nginx是lgor Sysoev为俄罗斯访问量第二的rambler.ru站点设计开发的.从2004年发布至今,凭借开源的力量,已经接近成熟与完善. Nginx功能丰富,可作为H ...

  6. 翻译 - ASP.NET Core 基本知识 - Web 主机 (Web Host)

    翻译自 https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/web-host?view=aspnetcore-5.0 ASP. ...

  7. [阿里DIEN] 深度兴趣进化网络源码分析 之 Keras版本

    [阿里DIEN] 深度兴趣进化网络源码分析 之 Keras版本 目录 [阿里DIEN] 深度兴趣进化网络源码分析 之 Keras版本 0x00 摘要 0x01 背景 1.1 代码进化 1.2 Deep ...

  8. Scrapy———反爬蟲的一些基本應對方法

    1. IP地址驗證 背景:有些網站會使用IP地址驗證進行反爬蟲處理,檢查客戶端的IP地址,若同一個IP地址頻繁訪問,則會判斷該客戶端是爬蟲程序. 解決方案: 1. 讓Scrapy不斷隨機更換代理服務器 ...

  9. 【Soul源码探秘】插件链实现

    引言 插件是 Soul 的灵魂. Soul 使用了插件化设计思想,实现了插件的热插拔,且极易扩展.内置丰富的插件支持,鉴权,限流,熔断,防火墙等等. Soul 是如何实现插件化设计的呢? 一切还得从插 ...

  10. 用git合并分支时,如何保持某些文件不被合并

    用git合并分支时,如何保持某些文件不被合并_fkaking的专栏-CSDN博客_git 合并分支 https://blog.csdn.net/fkaking/article/details/4495 ...