美文网首页呆鸟的Python数据分析呆鸟译Py
【译Py】Dash用户指南05_使用State进行回调

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

作者: 呆鸟的简书 | 来源:发表于2018-08-27 19:04 被阅读7次

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

    相关文章

      网友评论

      • b7d2007ef72f:dash应用只能有一个layout,如果有多种图要处理怎么做呢?只能用回调来做吗?
        呆鸟的简书:@gcc_eef0 这个内容我也暂时还没研究,如果你有心得,还请分享啊:smile:
      • 摸鱼哥:太棒了,如果可以的话,后续帮忙找些Dash的案例,这个工具很赞,想要在股票数据分析方面尝试用一下
        呆鸟的简书:@爱笑的小pp 这个可以必须有
      • 3a9f4e5cbe69:Dash后面还有更新吗?
        呆鸟的简书:@田雨2018 已经结束了,官网上就这么多,以后有机会我会再找更多内容

      本文标题:【译Py】Dash用户指南05_使用State进行回调

      本文链接:https://www.haomeiwen.com/subject/canwbftx.html