【Python百日基础系列】Day29 - Dash回调间共享数据

注意:本节内容需要Redis服务器支持,我用不大上,所以没有配置。

该dash库包含一个名为 的 Graph 组件dcc.Graph。
dcc.Graph使用开源plotly.js JavaScript 图形库呈现交互式数据可视化。Plotly.js 支持超过 35 种图表类型,并以矢量质量 SVG 和高性能 WebGL 呈现图表。
在dcc.Graph的figure参数分量是相同的,figure所使用的是plotly.py的参数。
正如我们已经看到的,Dash 组件由一组属性描述。这些属性中的任何一个都可以通过回调函数更新,但只有这些属性的一个子集通过用户交互更新,例如在dcc.Input组件内键入或单击组件中的选项dcc.Dropdown。
该dcc.Graph组件有四个可以通过用户交互更改的属性:hoverData、clickData、selectedData、 relayoutData。当您将鼠标悬停在点上、单击点或选择图形中的点区域时,这些属性会更新。

一、存储共享数据

为了在多个进程或服务器之间安全地共享数据,我们需要将数据存储在每个进程都可以访问的地方。
您可以在三个位置存储此数据:

  • 在用户的浏览器会话中,使用dcc.Store
  • 在磁盘上(例如在文件或数据库中)
  • 在跨进程和服务器(例如 Redis 数据库)共享的服务器端内存 (RAM) 中。为此,Dash Enterprise包含板载的一键式 Redis 数据库。

二、在浏览器中存储数据 dcc.Store

要在用户浏览器的会话中保存数据:

  1. 数据必须转换为字符串,如 JSON 或 base64 编码的二进制数据以进行存储
  2. 以这种方式缓存的数据将仅在用户的当前会话中可用。
  • 如果您打开一个新的浏览器窗口,应用程序的回调将始终重新计算数据。数据仅在同一会话内的回调之间缓存。
  • 此方法不会增加应用程序的内存占用。
  • 网络流量可能会产生成本。如果您在回调之间共享 10MB 的数据,那么该数据将在每个回调之间通过网络传输。
  • 如果网络成本太高,则预先计算聚合并传输它们。您的应用程序可能不会显示 10MB 的数据,它只会显示它的子集或聚合。
    下面的示例显示了您可以利用的一种常用方法dcc.Store:如果处理数据集需要很长时间并且不同的输出使用此数据集,dcc.Store则可用于将处理后的数据存储为中间值,然后可将其用作多个输入回调以生成不同的输出。这样,昂贵的数据处理步骤仅在一个回调中执行一次,而不是在每个回调中多次重复相同的昂贵计算。
  1. 如果数据很大,通过网络发送计算数据可能会很昂贵。在某些情况下,将此数据序列化为 JSON 的成本也很高。
  2. 在许多情况下,您的应用程序只会显示处理数据的子集或聚合。在这些情况下,您可以在数据处理回调中预先计算聚合并将这些聚合传输到剩余的回调。

三、缓存和信令

  1. 通过 Flask-Cache 使用 Redis 将“全局变量”存储在服务器端的数据库中。该数据通过函数 ( global_store())访问,该函数的输出由其输入参数缓存和键控。
  2. dcc.Store当昂贵的计算完成时,使用该解决方案向其他回调发送信号。
  3. 请注意,除了 Redis,您还可以将其保存到文件系统。
  4. 这种“signaling”是高性能的,因为它允许昂贵的计算只占用一个进程并执行一次。如果没有这种类型的信号,每个回调最终可能会并行计算昂贵的计算,锁定四个进程而不是一个。

3.1 代码

import os
import copy
import time
import datetime

import dash
from dash import dcc
from dash import html
import numpy as np
import pandas as pd
from dash.dependencies import Input, Output
from flask_caching import Cache


external_stylesheets = [
    # Dash CSS
    'https://codepen.io/chriddyp/pen/bWLwgP.css',
    # Loading screen CSS
    'https://codepen.io/chriddyp/pen/brPBPO.css']

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
server = app.server

CACHE_CONFIG = {
    # try 'FileSystemCache' if you don't want to setup redis
    'CACHE_TYPE': 'redis',
    'CACHE_REDIS_URL': os.environ.get('REDIS_URL', 'redis://localhost:6379')
}
cache = Cache()
cache.init_app(app.server, config=CACHE_CONFIG)

N = 100

df = pd.DataFrame({
    'category': (
        (['apples'] * 5 * N) +
        (['oranges'] * 10 * N) +
        (['figs'] * 20 * N) +
        (['pineapples'] * 15 * N)
    )
})
df['x'] = np.random.randn(len(df['category']))
df['y'] = np.random.randn(len(df['category']))

app.layout = html.Div([
    dcc.Dropdown(
        id='dropdown',
        options=[{'label': i, 'value': i} for i in df['category'].unique()],
        value='apples'
    ),
    html.Div([
        html.Div(dcc.Graph(id='graph-1'), className="six columns"),
        html.Div(dcc.Graph(id='graph-2'), className="six columns"),
    ], className="row"),
    html.Div([
        html.Div(dcc.Graph(id='graph-3'), className="six columns"),
        html.Div(dcc.Graph(id='graph-4'), className="six columns"),
    ], className="row"),

    # 触发回调的信号值
    dcc.Store(id='signal')
])


# 在“global store”中执行昂贵的计算
# 这些计算缓存在一个全局可用的数据库中
# 可跨进程和全时可使用的redis内存存储
@cache.memoize()
def global_store(value):
    # 模拟昂贵的查询
    print('Computing value with {}'.format(value))
    time.sleep(3)
    return df[df['category'] == value]


def generate_figure(value, figure):
    fig = copy.deepcopy(figure)
    filtered_dataframe = global_store(value)
    fig['data'][0]['x'] = filtered_dataframe['x']
    fig['data'][0]['y'] = filtered_dataframe['y']
    fig['layout'] = {'margin': {'l': 20, 'r': 10, 'b': 20, 't': 10} }
    return fig


@app.callback(Output('signal', 'data'), Input('dropdown', 'value'))
def compute_value(value):
    # 计算值并在完成时发送信号
    global_store(value)
    return value


@app.callback(Output('graph-1', 'figure'), Input('signal', 'data'))
def update_graph_1(value):
    # generate_figure 从 `global_store`中获取数据
    #  `global_store` 中的数据已经被计算过了
    # 通过 `compute_value` 回调
    # 结果存储在全局redis缓存中
    return generate_figure(value, {
        'data': [{
            'type': 'scatter',
            'mode': 'markers',
            'marker': {
                'opacity': 0.5,
                'size': 14,
                'line': {'border': 'thin darkgrey solid'}
            }
        }]
    })


@app.callback(Output('graph-2', 'figure'), Input('signal', 'data'))
def update_graph_2(value):
    return generate_figure(value, {
        'data': [{
            'type': 'scatter',
            'mode': 'lines',
            'line': {'shape': 'spline', 'width': 0.5},
        }]
    })


@app.callback(Output('graph-3', 'figure'), Input('signal', 'data'))
def update_graph_3(value):
    return generate_figure(value, {
        'data': [{
            'type': 'histogram2d',
        }]
    })


@app.callback(Output('graph-4', 'figure'), Input('signal', 'data'))
def update_graph_4(value):
    return generate_figure(value, {
        'data': [{
            'type': 'histogram2dcontour',
        }]
    })


if __name__ == '__main__':
    app.run_server(debug=True, processes=6)

3.2 页面效果

在这里插入图片描述

3.3 注意事项

我们通过使用 3 秒的系统睡眠模拟了一个昂贵的过程。
当应用程序加载时,渲染所有四个图形需要三秒钟。
初始计算只阻塞一个进程。
计算完成后,发送信号并并行执行四个回调以呈现图形。这些回调中的每一个都从“全局服务器端存储”中检索数据:Redis 或文件系统缓存。
我们已经设定processes=6在app.run_server使多个回调,可以并行执行。在生产中,这是通过类似$ gunicorn --workers 6 app:server. 如果您不使用多个进程运行,那么您将不会看到图形并行更新,因为回调将串行更新。
如果过去已选择,则在下拉列表中选择一个值将花费不到三秒钟的时间。这是因为该值是从缓存中提取的。
同样,重新加载页面或在新窗口中打开应用程序也很快,因为已经计算了初始状态和初始昂贵的计算。

四、服务器上基于用户的会话数据

4.1 代码

import dash
from dash.dependencies import Input, Output
from dash import dcc
from dash import html
import datetime
from flask_caching import Cache
import os
import pandas as pd
import time
import uuid

external_stylesheets = [
    # Dash CSS
    'https://codepen.io/chriddyp/pen/bWLwgP.css',
    # Loading screen CSS
    'https://codepen.io/chriddyp/pen/brPBPO.css']

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
cache = Cache(app.server, config={
    'CACHE_TYPE': 'redis',
    # 请注意,文件系统缓存不适用于具有短暂特性的系统
    # 像Heroku这样的文件系统。
    'CACHE_TYPE': 'filesystem',
    'CACHE_DIR': 'cache-directory',

    # 应等于一次应用程序上的最大用户数
    # 数字越大,文件系统/redis缓存中存储的数据就越多
    'CACHE_THRESHOLD': 200
})


def get_dataframe(session_id):
    @cache.memoize()
    def query_and_serialize_data(session_id):
        # 此处是昂贵的或用户/会话唯一的数据处理步骤
        # 通过生成依赖于时间的数据来模拟用户/会话唯一的数据处理步骤
        now = datetime.datetime.now()

        # 通过睡眠模拟昂贵的数据处理任务
        time.sleep(3)

        df = pd.DataFrame({
            'time': [
                str(now - datetime.timedelta(seconds=15)),
                str(now - datetime.timedelta(seconds=10)),
                str(now - datetime.timedelta(seconds=5)),
                str(now)
            ],
            'values': ['a', 'b', 'a', 'c']
        })
        return df.to_json()

    return pd.read_json(query_and_serialize_data(session_id))


def serve_layout():
    session_id = str(uuid.uuid4())

    return html.Div([
        dcc.Store(data=session_id, id='session-id'),
        html.Button('Get data', id='get-data-button'),
        html.Div(id='output-1'),
        html.Div(id='output-2')
    ])


app.layout = serve_layout


@app.callback(Output('output-1', 'children'),
              Input('get-data-button', 'n_clicks'),
              Input('session-id', 'data'))
def display_value_1(value, session_id):
    df = get_dataframe(session_id)
    return html.Div([
        'Output 1 - Button has been clicked {} times'.format(value),
        html.Pre(df.to_csv())
    ])


@app.callback(Output('output-2', 'children'),
              Input('get-data-button', 'n_clicks'),
              Input('session-id', 'data'))
def display_value_2(value, session_id):
    df = get_dataframe(session_id)
    return html.Div([
        'Output 2 - Button has been clicked {} times'.format(value),
        html.Pre(df.to_csv())
    ])


if __name__ == '__main__':
    app.run_server(debug=True)

4.2 页面效果

在这里插入图片描述

4.3 注意事项

  1. 当我们检索数据时,数据帧的时间戳不会更新。该数据作为用户会话的一部分被缓存。
  2. 检索数据最初需要三秒钟,但连续查询是即时的,因为数据已被缓存。
  3. 第二个会话显示与第一个会话不同的数据:回调之间共享的数据与各个用户会话隔离。
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Python Dash中,可以使用装饰器来用自身函数(self)。装饰器是一种装饰器模式,在Dash应用程序中使用它可以简化代码,增加可读性和可维护性。 当我们需要在Dash应用程序中使用函数时,可以使用`@app.callback`装饰器来定义函数,并将其与对应的输入和输出组件进行绑定。 首先,我们需要导入Dash库和装饰器: ``` import dash from dash.dependencies import Input, Output ``` 然后,创建Dash应用程序的实例: ``` app = dash.Dash(__name__) ``` 接下来,我们可以使用装饰器来定义函数。例如,我们定义一个简单的文本框和输出组件,当用户在文本框中输入内容时,输出组件会实时更新为输入的内容: ``` @app.callback( Output('output', 'children'), [Input('input', 'value')] ) def update_output(value): return value ``` 在上述示例中,`@app.callback`装饰器将`update_output`函数绑定到名为`output`的输出组件和名为`input`的输入组件上。`value`参数代表输入组件的值,`update_output`函数的返值将作为输出组件的内容。 通过这种方式,我们可以在函数中用自身函数。例如,我们可以在`update_output`函数中添加一些逻辑,根据输入的值进行判断并用自身函数。 注意,为了防止无限递归用,我们需要确保在适当的时候退出递归。可以使用条件语句或其他控制结构来实现这一点。 总而言之,Dash装饰器提供了一种方便的方式来定义和绑定函数。通过使用这些装饰器,我们可以在Dash应用程序中用自身函数,并实现更复杂的交互功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

岳涛@心馨电脑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值