(数据科学学习手札106)Python+Dash快速web应用开发——回调交互篇(下)
本文示例代码已上传至我的
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
import dash
import dash_bootstrap_components as dbc
import dash_html_components as html
from dash.dependencies import Input, Output
import time
app = dash.Dash(__name__)
app.layout = html.Div(
dbc.Container(
[
html.Br(),
html.Br(),
html.Br(),
dbc.Row(
dbc.Col(
dbc.Button('按钮',
color='primary',
id='button',
n_clicks=0)
)
),
html.Br(),
dbc.Row(
[
dbc.Col('尚未触发', id='record-1'),
dbc.Col('尚未触发', id='record-2'),
dbc.Col('尚未触发', id='record-n')
]
)
]
)
)
@app.callback(
[Output('record-1', 'children'),
Output('record-2', 'children'),
Output('record-n', 'children'),
],
Input('button', 'n_clicks'),
prevent_initial_call=True
)
def record_click_event(n_clicks):
if n_clicks == 1:
return (
'第1次点击:{}'.format(time.strftime('%H:%M:%S', time.localtime(time.time()))),
dash.no_update,
dash.no_update
)
elif n_clicks == 2:
return (
dash.no_update,
'第2次点击:{}'.format(time.strftime('%H:%M:%S', time.localtime(time.time()))),
dash.no_update
)
elif n_clicks >= 3:
return (
dash.no_update,
dash.no_update,
'第3次及以上点击:{}'.format(time.strftime('%H:%M:%S', time.localtime(time.time()))),
)
if __name__ == '__main__':
app.run_server(debug=True)
图2
可以观察到,我们根据n_clicks
数值的不同,在对应各个Output()
返回值中对符合条件的部件进行更新,其他的都用dash.no_update
来代替,从而实现了局部更新,非常实用且简单。
2.2 基于模式匹配的回调
这是Dash
在1.11.0版本开始引入的新特性,它所实现的功能是将多个部件绑定组织在同一个id
属性下,这听起来有一点抽象,我们先从一个形象的例子来出发:
假如我们要开发一个简单的记账应用,它通过第一排若干Input()
部件及一个Button()
部件来记录并提交每笔账对应的相关信息,并且在最下方输出已记录账目金额之和:
app2.py
import dash
import dash_bootstrap_components as dbc
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State, ALL
import re
app = dash.Dash(__name__)
app.layout = html.Div(
[
html.Br(),
html.Br(),
dbc.Container(
dbc.Row(
[
dbc.Col(
dbc.InputGroup(
[
dbc.InputGroupAddon("金额", addon_type="prepend"),
dbc.Input(
id='account-amount',
placeholder='请输入金额',
type="number",
),
dbc.InputGroupAddon("元", addon_type="append"),
],
),
width=5
),
dbc.Col(
dcc.Dropdown(
id='account-type',
options=[
{'label': '生活开销', 'value': '生活开销'},
{'label': '人情往来', 'value': '人情往来'},
{'label': '医疗保健', 'value': '医疗保健'},
{'label': '旅游休闲', 'value': '旅游休闲'},
],
placeholder='请选择类型:'
),
width=5
),
dbc.Col(
dbc.Button('提交记录', id='account-submit'),
width=2
)
]
)
),
html.Br(),
dbc.Container([], id='account-record-container'),
dbc.Container('暂无记录!', id='account-record-sum')
]
)
@app.callback(
Output('account-record-container', 'children'),
Input('account-submit', 'n_clicks'),
[State('account-record-container', 'children'),
State('account-amount', 'value'),
State('account-type', 'value')],
prevent_initial_call=True
)
def update_account_records(n_clicks, children, account_amount, account_type):
'''
用于处理每一次的记账输入并渲染前端记录
'''
if account_amount and account_type:
children.append(dbc.Row(
dbc.Col(
'【{}】类开销【{}】元'.format(account_type, account_amount)
),
# 以字典形式定义id
id={'type': 'single-account_record', 'index': children.__len__()}
))
return children
@app.callback(
Output('account-record-sum', 'children'),
Input({'type': 'single-account_record', 'index': ALL}, 'children'),
prevent_initial_call=True
)
def refresh_account_sum(children):
'''
对多部件集合single-account_record下所有账目记录进行求和
'''
return '账本总开销:{}'.format(sum([int(re.findall('\d+',
child['props']['children'])[0])
for child in children]))
if __name__ == '__main__':
app.run_server(debug=True)
图3
上面这个应用中,体现出的模式匹配内容即为开头从dash.dependencies
引入的ALL
,它是Dash
模式匹配中的一种模式,而我们在回调函数update_account_records()
中为已有记账记录追加新纪录时,使用到:
# 以字典形式定义id
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
import dash
import dash_bootstrap_components as dbc
import dash_html_components as html
from dash.dependencies import Input, Output, State, MATCH
import dash_core_components as dcc
app = dash.Dash(__name__)
app.layout = html.Div(
[
html.Br(),
html.Br(),
html.Br(),
dbc.Container(
[
dbc.Row(
dbc.Col(
dbc.Button('新增查询', id='add-item', outline=True)
)
),
html.Hr()
]
),
dbc.Container([], id='query-container')
]
)
region2code = {
'北京市': '110000000000',
'重庆市': '500000000000',
'安徽省': '340000000000'
}
@app.callback(
Output('query-container', 'children'),
Input('add-item', 'n_clicks'),
State('query-container', 'children'),
prevent_initial_call=True
)
def add_query_item(n_clicks, children):
children.append(
dbc.Row(
[
dbc.Col(
[
# 生成index相同的dropdown部件与文字输出部件
dcc.Dropdown(id={'type': 'select-province', 'index': children.__len__()},
options=[{'label': label, 'value': label} for label in region2code.keys()],
placeholder='选择省份:'),
html.P('请输入要查询的省份!', id={'type': 'code-output', 'index': children.__len__()})
]
)
]
)
)
return children
@app.callback(
Output({'type': 'code-output', 'index': MATCH}, 'children'),
Input({'type': 'select-province', 'index': MATCH}, 'value')
)
def refresh_code_output(value):
if value:
return region2code[value]
else:
return dash.no_update
if __name__ == '__main__':
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
import dash
import dash_html_components as html
import dash_bootstrap_components as dbc
from dash.dependencies import Input, Output
import json
app = dash.Dash(__name__)
app.layout = html.Div(
dbc.Container(
[
html.Br(),
html.Br(),
html.Br(),
dbc.Row(
[
dbc.Col(dbc.Button('A', id='A', n_clicks=0)),
dbc.Col(dbc.Button('B', id='B', n_clicks=0)),
dbc.Col(dbc.Button('C', id='C', n_clicks=0))
]
),
dbc.Row(
[
dbc.Col(html.P('按钮A未点击', id='A-output')),
dbc.Col(html.P('按钮B未点击', id='B-output')),
dbc.Col(html.P('按钮C未点击', id='C-output'))
]
),
dbc.Row(
dbc.Col(
html.Pre(id='raw-json')
)
)
]
)
)
@app.callback(
[Output('A-output', 'children'),
Output('B-output', 'children'),
Output('C-output', 'children'),
Output('raw-json', 'children')],
[Input('A', 'n_clicks'),
Input('B', 'n_clicks'),
Input('C', 'n_clicks')],
prevent_initial_call=True
)
def refresh_output(A_n_clicks, B_n_clicks, C_n_clicks):
# 获取本轮回调状态下的上下文信息
ctx = dash.callback_context
# 取出对应State、最近一次触发部件以及Input信息
ctx_msg = json.dumps({
'states': ctx.states,
'triggered': ctx.triggered,
'inputs': ctx.inputs
}, indent=2)
return A_n_clicks, B_n_clicks, C_n_clicks, ctx_msg
if __name__ == '__main__':
app.run_server(debug=True)
图5
可以看到,我们安插在回调函数里的dash.callback_context
帮我们记录了从访问Dash
开始,到最近一次执行回调期间,对应回调的输入输出信息变化情况、最近一次触发信息,非常的实用,可以支撑起很多复杂应用场景。
2.4 在浏览器端执行回调过程
Dash
虽然很方便,使得我们可以完全不用书写js
代码就可以实现各种回调交互,但把所有的交互响应计算过程都交给服务端来做,省事倒是很省事,但会给服务器带来不小的计算和网络传输压力。
因此很多容易频繁触发且与主要的数值计算无关的交互行为,完全可以搬到浏览器端执行,既快速又不吃服务器的计算资源,这也是当初JavaScript
被发明的一个重要原因,而在Dash
中,也为略懂js
的用户提供了在浏览器端执行一些回调的贴心功能。
从一个很简单的点击按钮,实现部分网页内容的打开与关闭出发,这里我们提前使用到dbc.Collapse
部件,用于将所包含的网页内容与其它按钮部件的点击行为进行绑定:
app5.py
import dash
import dash_bootstrap_components as dbc
import dash_html_components as html
from dash.dependencies import Input, Output, State
app = dash.Dash(__name__)
app.layout = html.Div(
dbc.Container(
[
html.Br(),
html.Br(),
html.Br(),
dbc.Button('服务端回调', id='server-button'),
dbc.Collapse('服务端折叠内容', id='server-collapse'),
html.Hr(),
dbc.Button('浏览器端回调', id='browser-button'),
dbc.Collapse('浏览器端折叠内容', id='browser-collapse'),
]
)
)
@app.callback(
Output('server-collapse', 'is_open'),
Input('server-button', 'n_clicks'),
State('server-collapse', 'is_open'),
prevent_initial_call=True
)
def server_callback(n_clicks, is_open):
return not is_open
# 在dash中定义浏览器端回调函数的特殊格式
app.clientside_callback(
"""
function(n_clicks, is_open) {
return !is_open;
}
""",
Output('browser-collapse', 'is_open'),
Input('browser-button', 'n_clicks'),
State('browser-collapse', 'is_open'),
prevent_initial_call=True
)
if __name__ == '__main__':
app.run_server(debug=True)
可以看到,服务端回调我们照常写,而浏览器端回调通过传入一个非常简单的js
函数,在每次回调时接受输入并输出is_open
的逻辑反值,从而实现了折叠内容的打开与关闭切换:
function(n_clicks, is_open) {
return !is_open;
}
便实现了浏览器端回调!
图6
而如果你想要执行的浏览器端js
回调函数代码有点长,还可以按照下图格式,把你的大段js
回调函数代码放置于assets
目录下对应路径里的js
脚本中:
图7
接着再在dash
中按照下列格式编写关联输入输出与上述js
回调的简短语句即可:
app.clientside_callback(
ClientsideFunction(
namespace='命名空间名称',
function_name='对应js回调函数名'
),
'''
按顺序组织你的Output、Input以及State... ...
'''
)
下面我们直接以大家喜闻乐见的数据可视化顶级框架echarts
为例,来写一个根据不同输入值切换渲染出的图表类型,注意请从官网把依赖的echarts.min.js
下载到我们的assets
路径下对应位置,它会在我们的Dash
应用启动时与所有assets
下的资源一起自动被载入到浏览器中:
app6.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, ClientsideFunction
app = dash.Dash(__name__)
# 编写一个根据dropdown不同输入值切换对应图表类型的小应用
app.layout = html.Div(
dbc.Container(
[
html.Br(),
dbc.Row(
dbc.Col(
dcc.Dropdown(
id='chart-type',
options=[
{'label': '折线图', 'value': '折线图'},
{'label': '堆积面积图', 'value': '堆积面积图'},
],
value='折线图'
),
width=3
)
),
html.Br(),
dbc.Row(
dbc.Col(
html.Div(
html.Div(
id='main',
style={
'height': '100%',
'width': '100%'
}
),
style={
'width': '800px',
'height': '500px'
}
)
)
)
]
)
)
app.clientside_callback(
# 关联自编js脚本中的相应回调函数
ClientsideFunction(
namespace='clientside',
function_name='switch_chart'
),
Output('main', 'children'),
Input('chart-type', 'value')
)
if __name__ == '__main__':
app.run_server(debug=True)
图8
效果十分惊人,从此我们使用Dash
不仅仅可以使用Python
生态的工具,还可以配合对前端内容支持更好的js
,起飞!
至此我们的Dash
回调交互三部曲已结束,接下来的文章我将开始带大家遨游丰富的各种Dash
前端部件,涵盖了网页部件、数据可视化图表以及地图可视化等内容,敬请期待这场奇妙之旅吧~
以上就是本文的全部内容,欢迎在评论区与我进行讨论。
(数据科学学习手札106)Python+Dash快速web应用开发——回调交互篇(下)的更多相关文章
- (数据科学学习手札105)Python+Dash快速web应用开发——回调交互篇(中)
本文示例代码已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 这是我的系列教程Python+Dash快速web ...
- (数据科学学习手札104)Python+Dash快速web应用开发——回调交互篇(上)
本文示例代码已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 这是我的系列教程Python+Dash快速web ...
- (数据科学学习手札102)Python+Dash快速web应用开发——基础概念篇
本文示例代码与数据已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 这是我的新系列教程Python+Dash快 ...
- (数据科学学习手札108)Python+Dash快速web应用开发——静态部件篇(上)
本文示例代码已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 这是我的系列教程Python+Dash快速web ...
- (数据科学学习手札109)Python+Dash快速web应用开发——静态部件篇(中)
本文示例代码已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 这是我的系列教程Python+Dash快速web ...
- (数据科学学习手札118)Python+Dash快速web应用开发——特殊部件篇
本文示例代码已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 这是我的系列教程Python+Dash快速web ...
- (数据科学学习手札103)Python+Dash快速web应用开发——页面布局篇
本文示例代码已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 这是我的系列教程Python+Dash快速web ...
- (数据科学学习手札110)Python+Dash快速web应用开发——静态部件篇(下)
本文示例代码已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 这是我的系列教程Python+Dash快速web ...
- (数据科学学习手札123)Python+Dash快速web应用开发——部署发布篇
1 简介 这是我的系列教程Python+Dash快速web应用开发的第二十期,在上一期中我介绍了利用内网穿透的方式,将任何可以联网的电脑作为"服务器"向外临时发布你的Dash应用. ...
随机推荐
- Nginx 路由转发和反向代理 location 配置
Nginx 配置的三种方式 第一种直接替换 location 匹配部分 第二种 proxy_pass 的目标地址,默认不带 /,表示只代理域名,url 和参数部分不会变(把请求的 path 拼接到 p ...
- SQL语句中 ` 的作用
SQL语句中 ` 的作用 做攻防世界WEB区 supersqli 题目,在构建SQL语句时,遇到SQL语句中有 ` 时可以解析,没有则不能. 查阅资料得知,` 通常用来说明其中的内容是数据库名.表名. ...
- SDUST数据结构 - chap4 串
函数题: 6-1 查找子串: 裁判测试程序样例: #include <stdio.h> #define MAXS 30 char *search(char *s, char *t); vo ...
- ctfhub技能树—信息泄露—git泄露—Log
什么是git泄露? 当前大量开发人员使用git进行版本控制,对站点自动部署.如果配置不当,可能会将.git文件夹直接部署到线上环境.这就引起了git泄露漏洞. 打开靶机环境 查看网页内容 使用dirs ...
- SAP下载文档为乱码
通过事物WE60下载的文档为乱码,主要原因是编码格式的不匹配,通常默认的编码格式为ANSI编码,那么我们需要将源码的编码格式转换成UTF-8,这样问题可以解决了. 附:编码格式介绍 不同的国家和地 ...
- 面试官问我CAS,我一点都不慌
文章以纯面试的角度去讲解,所以有很多的细节是未铺垫的. 文章中写到的处理线程安全的思路每一项技术都可以写出一篇文章,AQS.Synchronized.Atomic...周末肝起来!下周再来给大家安排! ...
- LuoguP5488 差分与前缀和
题意 给定一个长为\(n\)的序列\(a\),求出其\(k\)阶差分或前缀和.结果的每一项都需要对\(1004535809\)取模. 打表找规律 先看前缀和,设\(n=5\),\(k=4\),按照阶从 ...
- Pku1236 Network of Schools
题目描述 n个学校构成一个有向图,通过m条边连接,一:问至少向图中多少个学校投放软件,可以使得所有学校直接或者间接的通过边(假设存在边(u,v),则向u投放v可以得到,而向v投放u不能通过v直接得到) ...
- 超精讲-逐例分析 CSAPP:实验2-Bomb!(下)
好了话不多说我们书接上文继续来做第二个实验下面是前半部分实验的连接 5. 第五关 首先感觉应该是个递归问题 /* Round and 'round in memory we go, where we ...
- 使用amoeba实现mysql读写分离
使用amoeba实现mysql读写分离 1.什么是amoeba? Amoeba(变形虫)项目,专注 分布式数据库 proxy 开发.座落与Client.DB Server(s)之间.对客户端透明. ...