【呆鳥譯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)
本例中,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)
改變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個位置存儲數據:
用戶瀏覽器會話;
硬盤上,比如,文件或新建數據庫;
像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進行回調