Python的Web可視化框架Dash(7)---回調共享

【Dash系列】Python的Web可視化框架Dash(1)---簡介
【Dash系列】Python的Web可視化框架Dash(2)---安裝
【Dash系列】Python的Web可視化框架Dash(3)---布局設置
【Dash系列】Python的Web可視化框架Dash(4)---基本回調
【Dash系列】Python的Web可視化框架Dash(5)---狀態和預更新
【Dash系列】Python的Web可視化框架Dash(6)---交互和過濾
【Dash系列】Python的Web可視化框架Dash(7)---回調共享
【Dash系列】Python的Web可視化框架Dash(8)---核心組件

本節介紹Dash應用回調之間共享狀態和數據,導入本節用到的所有包
import pandas as pd
import plotly.graph_objs as go
import dash
import dash_core_components as dcc                  # 交互式組件
import dash_html_components as html                 # 代碼轉html
from dash.dependencies import Input, Output         # 回調
from jupyter_plotly_dash import JupyterDash         # Jupyter中的Dash


一、概述

不同回調函數之間共享狀態

回調入門指南 中提到過Dash的核心原則之一 : Dash Callbacks 絕不能修改其范圍之外的變量。修改任何 global 變量都是不安全的。本章解釋這樣操作為什么不安全,并提出在回調函數間共享狀態的替代方式。

為什么要共享狀態?

某些應用可能會有SQL查詢、運行模擬或下載數據等擴展性數據處理任務,所以會使用多個回調函數。

與其讓每個回調函數都運行同一個大規模運算任務,不如讓其中一個回調函數執行任務,然后將結果共享給其它回調函數,而不是讓每個回調函數運行相同的昂貴任務。

由于可以為一次回調設置多個輸出,實現昂貴的任務一次完成,即可用于所有的輸出。但在某些情況下,這仍然不太理想,例如,如果有簡單的后續任務可以修改結果,例如單位轉換。我們不需要重復大型數據庫查詢,只是為了將結果從華氏溫度更改為攝氏溫度。

為什么全局變量會破壞應用?

Dash應用旨在在多用戶環境中工作,多個人可以同時查看應用程序,并具有獨立會話。

如果用戶可以修改應用的全局變量,即使用修改后的 global 變量,會影響下一位用戶會話的值。

Dash的設計思路還包括運行多個Python workers,實現多個回調函數,并行執行。通常,使用gunicorn語法完成:

$ gunicorn --workers 4 app:server
  • app:命名的文件app.py
  • server:命名的文件的變量 server:server = app.server

當Dash應用跨多個工作程序(worker)運行時,不會共享內存。這意味著,如果某個回調函數修改了全局變量,則該修改將不會應用于其它的工作程序。

在回調函數之間共享數據

為了在多個python進程之間安全地共享數據,則需要將數據存儲在每個進程可訪問的位置。推薦在如下3個位置存儲數據:

  • 在用戶的瀏覽器會話中
  • 在磁盤上,例如:文件或新數據庫
  • 與Redis一樣,存在共享內存空間

示例說明

下面的例子,展示了回調函數在其應用范圍外修改數據。鑒于上述原因,它的運行結果可能不靠譜。

df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 1, 4], 'c': ['x', 'y', 'z']})

app = JupyterDash('wrong_id', height = 100)
app.layout = html.Div([
    dcc.Dropdown(
        id = 'dropdown_id',
        options = [{'label': i, 'value': i} for i in df['c'].unique()],
        value = 'x'),
    html.Div(id = 'output_id')
])

@app.callback(Output('output_id', 'children'), [Input('dropdown_id', 'value')])
def update_output(value):
    global df = df[df['c'] == value]    # 不要這樣做,這不安全!
    return len(df)

app

要修復此示例,只需要將篩選器,重新分配給回調內的新變量,或者按照上述的“回調函數之間共享數據”介紹的任一方法,進行操作。

df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 1, 4], 'c': ['x', 'y', 'z']})

app = JupyterDash('wrong_id', height = 100)
app.layout = html.Div([
    dcc.Dropdown(
        id = 'dropdown_id',
        options = [{'label': i, 'value': i} for i in df['c'].unique()],
        value = 'x'),
    html.Div(id = 'output_id')
])

@app.callback(Output('output_id', 'children'), [Input('dropdown_id', 'value')])
def update_output(value):
    filtered_df = df[df['c'] == value]    # 安全地將篩選器,重新分配給新變量
    return len(filtered_df)

app


二、在瀏覽器中存儲數據

(一) 代碼

global_df = pd.read_csv('...')

app = JupyterDash('browser_id')
app.layout = html.Div([
    dcc.Graph(id = 'graph_id'),
    html.Table(id = 'table_id'),
    dcc.Dropdown(id = 'dropdown_id'),
    html.Div(id = 'intermediate-value', style = dict(display = None))    # 用于存儲中間值的Hidden Div
])

@app.callback(Output('intermediate-value', 'children'), [Input('dropdown_id', 'value')])
def clean_data(value):
    cleaned_df = your_expensive_clean_or_compute_step(value)    # 清理大規模數據
    return cleaned_df.to_json(date_format = 'iso', orient = 'split')    # 轉化成Json格式

@app.callback(Output('graph_id', 'children'), [Input('intermediate-value', 'children')])
def update_graph(jsonified_cleaned_data):
    dff = pd.read_json(jsonified_cleaned_data, orient='split')    # 將Json數據讀取為dataframe
    fig = create_figure(dff)    # 生成表格圖表
    return fig

@app.callback(Output('table_id', 'children'), [Input('intermediate-value', 'children')])
def update_table(jsonified_cleaned_data):
    dff = pd.read_json(jsonified_cleaned_data, orient='split')    # 將Json數據讀取為dataframe
    table = create_table(dff)    # 生成表格圖表
    return table

app

(二) 說明

  1. 本例介紹了在回調函數中,執行大規模數據處理的步驟:以JSON格式進行序列化輸出,并將其作為其他回調函數的輸入。本例使用標準的Dash回調函數,將JSON數據存儲在應用的Hidden Div里;

  2. 通過Dash 社區中提供的方法,將數據保存為Dash前端的一部分;

  3. 必須將數據轉換為類似JSON的字符串,才能進行存儲和傳輸;

  4. 以這種方式緩存的數據,僅在用戶的當前會話中可用(生效):

  • 數據僅在當前會話內的回調函數中緩存和傳輸。如果打開了一個新的瀏覽器,則應用程序的回調函數會重新計算;
  • 與緩存不同,這種方法不會增加應用程序的內存占用量;
  • 網絡傳輸會產生成本。如果在回調函數之間,共享小于等于10M的數據,那么每次回調時,都會通過網絡進行數據傳輸;
  • 如果網絡成本太高,可以先做聚合計算再傳輸數據。 應用程序一般不會顯示多于10MB的數據,大部分情況下,只顯示其子集或聚合結果。

三、預先聚合或篩選

(一) 代碼

@app.callback(Output('intermediate-value', 'children'), [Input('dropdown_id', '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_id', '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_id', '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_id', '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

(二) 說明

  1. 這是一個簡單的示例,說明如何將聚合或篩選的數據傳輸給多個回調函數;
  2. 如果數據量過大,通過網絡發送運算后的數據,代價會很高。在某些情況下,即便將數據序列化或使用JSON格式,其運算量也很大;
  3. 通常數情況下,Dash應用可以在處理回調時,預先對數據進行計算、篩選、聚合,再將這些結果子集,傳輸給剩下的其它回調中。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容