【呆鳥譯Py】Dash用戶指南05_使用State進行回調

【呆鳥譯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進行回調

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。