【呆鸟译Py】Dash用户指南05_使用State进行回调

96
呆鸟的简书
2018.08.27 19:04* 字数 2071

【呆鸟译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)
015

本例中,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)
016

改变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个位置存储数据:

  1. 用户浏览器会话;

  2. 硬盘上,比如,文件或新建数据库;

  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进行回调

呆鸟译Py
Gupao