【译Py】Python交互式数据分析报告框架~Dash介绍
【译Py】Dash用户指南01-02_安装与应用布局
【译Py】Dash用户指南03_交互性简介
【译Py】Dash用户指南04_交互式数据图
【译Py】Dash用户指南05_使用State进行回调
5. 使用State进行回调
前面章节里介绍的Dash回调函数基础中,回调函数是这样的:
# -*- coding: utf-8 -*-
import dash
from dash.dependencies import Input, Output
import dash_core_components as dcc
import dash_html_components as html
app = dash.Dash(__name__)
app.css.append_css(
{"external_url": "https://codepen.io/chriddyp/pen/bWLwgP.css"})
app.layout = html.Div([
dcc.Input(id='input-1', type='text', value='北京'),
dcc.Input(id='input-2', type='text', value='中国'),
html.Div(id='output')
])
@app.callback(Output('output', 'children'),
[Input('input-1', 'value'),
Input('input-2', 'value')])
def update_output(input1, input2):
return '第一个输入项是"{}",第二个输入项是"{}"'.format(input1, input2)
if __name__ == '__main__':
app.run_server(debug=True)
本例中,dash.dependencies.Input
的属性变化会激活回调函数。在文本框中输入数据,可以看到这一效果。
dash.dependencies.State
允许传递额外值而不激活回调函数。这个例子和上例基本一样,只是将dcc.Input
替换为 dash.dependencies.State
,将按钮替换为dash.dependencies.Input
。
# -*- coding: utf-8 -*-
import dash
from dash.dependencies import Input, Output, State
import dash_core_components as dcc
import dash_html_components as html
app = dash.Dash()
app.css.append_css(
{"external_url": "https://codepen.io/chriddyp/pen/bWLwgP.css"})
app.layout = html.Div([
dcc.Input(id='input-1-state', type='text', value='北京'),
dcc.Input(id='input-2-state', type='text', value='中国'),
html.Button(id='submit-button', n_clicks=0, children='提交'),
html.Div(id='output-state')
])
@app.callback(Output('output-state', 'children'),
[Input('submit-button', 'n_clicks')],
[State('input-1-state', 'value'),
State('input-2-state', 'value')])
def update_output(n_clicks, input1, input2):
return u'''
已经点击了{}次按钮,
第一个输入项是"{}",
第二个输入项是"{}"
'''.format(n_clicks, input1, input2)
if __name__ == '__main__':
app.run_server(debug=True)
改变dcc.Input
文本框中的文本不会激活回调函数,点击提交按钮才会激活回调函数。即使不激活回调函数本身,dcc.Input
的现值依然会传递给回调函数。
注意,在本例中,触发回调是通过监听html.Button
组件的n_clicks
特性实现的,每次单击组件时,n_clicks
都会增加, 这个功能适用于dash_html_components
库里的所有组件。
在不同回调函数之间共享状态
回调函数入门里提到过Dash的核心原则是绝对不要在变量范围之外修改Dash回调函数的变量。修改任何全局变量都不安全。本章解释这样操作为什么不安全,并提出在回调函数间共享状态的替代方式。
为什么要共享状态?
某些应用会有SQL查询、运行模拟或下载数据等扩展性数据处理任务,所以会使用多个回调函数。
与其让每个回调函数都运行同一个大规模运算任务,不如让其中一个回调函数执行任务,然后将结果共享给其它回调函数。
为什么全局
变量会破坏应用
Dash的设计思路是实现在多用户环境下,多人可以同时查看应用,这就有了独立会话的概念。
如果用户可以修改应用的全局变量,前一个用户的会话就会重置全局变量,从而影响下一位用户会话的值。
Dash的设计思路还包括运行多个Python workers,以便多个回调函数能够并行。这种情况一般使用gunicorn
语法来实现。
$ gunicorn --workers 4 --threads 2 app:server
Dash应用跨多个worker运行时,不会共享内存,这意味着如果某个回调函数修改了全局变量,其改动不会应用于其它worker。
下面的例子展示了回调函数在其应用范围外修改数据。鉴于上述原因,它的运行结果可能不靠谱。
df = pd.DataFrame({
'a': [1, 2, 3],
'b': [4, 1, 4],
'c': ['x', 'y', 'z'],
})
app.layout = html.Div([
dcc.Dropdown(
id='dropdown',
options=[{'label': i, 'value': i} for i in df['c'].unique()],
value='a'
),
html.Div(id='output'),
])
@app.callback(Output('output', 'children'),
[Input('dropdown', 'value')])
def update_output_1(value):
# 这里, `df` 是变量在函数范围之外的例子。
# 在回调中修改或重新分配这个变量不安全。
global df = df[df['c'] == value] # 不要这么干,不安全!
return len(df)
要修复这个问题,只需为回调函数内的新变量再指定一个筛选器即可,可以使用下面的方法。
df = pd.DataFrame({
'a': [1, 2, 3],
'b': [4, 1, 4],
'c': ['x', 'y', 'z'],
})
app.layout = html.Div([
dcc.Dropdown(
id='dropdown',
options=[{'label': i, 'value': i} for i in df['c'].unique()],
value='a'
),
html.Div(id='output'),
])
@app.callback(Output('output', 'children'),
[Input('dropdown', 'value')])
def update_output_1(value):
# 为新变量指定筛选器,这样做是安全的
filtered_df = df[df['c'] == value]
return len(filtered_df)
在回调函数之间共享数据
为了安全地跨多个python进程共享数据,需要将数据存储在每个进程都能访问的位置。 建议在这3个位置存储数据:
用户浏览器会话;
硬盘上,比如,文件或新建数据库;
像Redis一样,存在共享内存空间。
下面几个例子详细说明了这三种方法。
例1 在Hidden Div中存储数据
为了在用户浏览器会话里保存数据,需要:
- 通过https://community.plot.ly/t/sharing-a-dataframe-between-plots/6173里的方法,将数据保存为Dash前端的一部分;
- 将数据转换为JSON文本格式,然后进行存储和传输;
- 以这种方式缓存的数据只在当前用户会话中生效;
- 打开新的浏览器页面后,回调函数用会计算数据。该数据仅在当前会话的回调函数中缓存和传输;
- 与缓存不同,这种方法不会增加对内存的占用;
- 网络传输会产生成本。假如在回调函数之间共享10MB数据,每次回调时都会通过网络传输数据。
- 如果网络成本太高,可以先做聚合计算再传输数据。 应用一般不会显示多于10MB的数据,大部分情况下只显示子集或子集的聚合结果。
本例概述了在回调函数中执行大规模的数据处理步骤,以JSON格式进行序列化输出,并将其作为其他回调函数的输入。本例使用标准Dash回调函数,将JSON数据存储在应用的Hidden Div里。
global_df = pd.read_csv('...')
app.layout = html.Div([
dcc.Graph(id='graph'),
html.Table(id='table'),
dcc.Dropdown(id='dropdown'),
# 用于存储中间值的Hidden Div。
html.Div(id='intermediate-value', style={'display': 'none'})
])
@app.callback(Output('intermediate-value', 'children'), [Input('dropdown', 'value')])
def clean_data(value):
# 清理大规模数据的步骤
cleaned_df = your_expensive_clean_or_compute_step(value)
# 通常使用下列语句
# json.dumps(cleaned_df)
return cleaned_df.to_json(date_format='iso', orient='split')
@app.callback(Output('graph', 'figure'), [Input('intermediate-value', 'children')])
def update_graph(jsonified_cleaned_data):
# 通常使用下列语句
# json.loads(jsonified_cleaned_data)
dff = pd.read_json(jsonified_cleaned_data, orient='split')
figure = create_figure(dff)
return figure
@app.callback(Output('table', 'children'), [Input('intermediate-value', 'children')])
def update_table(jsonified_cleaned_data):
dff = pd.read_json(jsonified_cleaned_data, orient='split')
table = create_table(dff)
return table
例2 预聚合计算
如果数据量过大,即使通过网络发送运算后的数据代价也会很高。 在某些情况下,即便将数据序列化或使用JSON格式的运算量也很大。
很多情况下,Dash应用只显示经过计算、过滤的数据子集或聚合结果。 这样就可以在处理回调时,对数据进行聚合预计算,将聚合结果传输给其它回调函数即可。
下面是将过滤或聚合过的数据传输给多个回调函数的例子。
@app.callback(
Output('intermediate-value', 'children'),
[Input('dropdown', 'value')])
def clean_data(value):
# 高消耗的查询步骤
cleaned_df = your_expensive_clean_or_compute_step(value)
# 为了计算后期回调函数所需的数据而进行的筛选
df_1 = cleaned_df[cleaned_df['fruit'] == 'apples']
df_2 = cleaned_df[cleaned_df['fruit'] == 'oranges']
df_3 = cleaned_df[cleaned_df['fruit'] == 'figs']
datasets = {
'df_1': df_1.to_json(orient='split', date_format='iso'),
'df_2': df_2.to_json(orient='split', date_format='iso'),
'df_3': df_3.to_json(orient='split', date_format='iso'),
}
return json.dumps(datasets)
@app.callback(
Output('graph', 'figure'),
[Input('intermediate-value', 'children')])
def update_graph_1(jsonified_cleaned_data):
datasets = json.loads(jsonified_cleaned_data)
dff = pd.read_json(datasets['df_1'], orient='split')
figure = create_figure_1(dff)
return figure
@app.callback(
Output('graph', 'figure'),
[Input('intermediate-value', 'children')])
def update_graph_2(jsonified_cleaned_data):
datasets = json.loads(jsonified_cleaned_data)
dff = pd.read_json(datasets['df_2'], orient='split')
figure = create_figure_2(dff)
return figure
@app.callback(
Output('graph', 'figure'),
[Input('intermediate-value', 'children')])
def update_graph_3(jsonified_cleaned_data):
datasets = json.loads(jsonified_cleaned_data)
dff = pd.read_json(datasets['df_3'], orient='split')
figure = create_figure_3(dff)
return figure
例3 缓存与信令(Signaling)
本例说明:
- 使用Flask-Cache插件在Redis中存储全局变量。 通过函数访问数据,通过该函数的输入参数对输出项进行缓存与键入处理。
- 大规模运算完成后,将Hidden Div里存储的数据发送信令给其它回调函数。
- 注意,如果不用Redis,可以将数据保存至文件系统。详细内容请参阅:https://flask-caching.readthedocs.io/en/latest/。
- 因为允许大规模运算占用一个进程,所以使用信令这种方式没什么问题。如果不使用信令,每个回调函数都要进行并行的大规模运算,这样锁定的就不是1个进程,而是4个进程了。
这种方法的另一个优点是,下一个会话可以使用预计算的值。如果输入数量不多的话,对应用的运行有很大好处。
下面是这个例子运行后的示意图。需要注意以下几点:
- 使用time.sleep(5)模拟大规模运算进程;
- 加载应用时,需要5秒渲染所有4副图;
- 初始运算仅阻断1个进程;
- 运算完成后,发送信令,并行执行4个回调函数渲染图形。每个回调函数都从全局存储,即Redis的缓存中提取数据;
- 在app.run里面设置processes = 6,即允许多个回调函数并行执行。在生产环境中,使用
$ gunicorn --workers 6 --threads 2 app:server
实现类似的效果; - 如果之前已经选择过,再在下拉菜单选择值不会超过5秒,这是因为已经预先从缓存中把备选值提取出来了;
- 与此类似,重新加载页面或在新窗口中打开应用也会比较快,这是因为初始状态和初始的大规模运算已经执行完毕了。
【译Py】Python交互式数据分析报告框架~Dash介绍
【译Py】Dash用户指南01-02_安装与应用布局
【译Py】Dash用户指南03_交互性简介
【译Py】Dash用户指南04_交互式数据图
【译Py】Dash用户指南05_使用State进行回调