【呆鳥(niǎo)譯Py】Python交互式數(shù)據(jù)分析報(bào)告框架~Dash介紹
【呆鳥(niǎo)譯Py】Dash用戶(hù)指南01-02_安裝與應(yīng)用布局
【呆鳥(niǎo)譯Py】Dash用戶(hù)指南03_交互性簡(jiǎn)介
【呆鳥(niǎo)譯Py】Dash用戶(hù)指南04_交互式數(shù)據(jù)圖
【呆鳥(niǎo)譯Py】Dash用戶(hù)指南05_使用State進(jìn)行回調(diào)
5. 使用State進(jìn)行回調(diào)
前面章節(jié)里介紹的Dash回調(diào)函數(shù)基礎(chǔ)中,回調(diào)函數(shù)是這樣的:
# -*- 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='中國(guó)'),
html.Div(id='output')
])
@app.callback(Output('output', 'children'),
[Input('input-1', 'value'),
Input('input-2', 'value')])
def update_output(input1, input2):
return '第一個(gè)輸入項(xiàng)是"{}",第二個(gè)輸入項(xiàng)是"{}"'.format(input1, input2)
if __name__ == '__main__':
app.run_server(debug=True)

本例中,dash.dependencies.Input的屬性變化會(huì)激活回調(diào)函數(shù)。在文本框中輸入數(shù)據(jù),可以看到這一效果。
dash.dependencies.State 允許傳遞額外值而不激活回調(diào)函數(shù)。這個(gè)例子和上例基本一樣,只是將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='中國(guó)'),
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'''
已經(jīng)點(diǎn)擊了{(lán)}次按鈕,
第一個(gè)輸入項(xiàng)是"{}",
第二個(gè)輸入項(xiàng)是"{}"
'''.format(n_clicks, input1, input2)
if __name__ == '__main__':
app.run_server(debug=True)

改變dcc.Input文本框中的文本不會(huì)激活回調(diào)函數(shù),點(diǎn)擊提交按鈕才會(huì)激活回調(diào)函數(shù)。即使不激活回調(diào)函數(shù)本身,dcc.Input的現(xiàn)值依然會(huì)傳遞給回調(diào)函數(shù)。
注意,在本例中,觸發(fā)回調(diào)是通過(guò)監(jiān)聽(tīng)html.Button組件的n_clicks特性實(shí)現(xiàn)的,每次單擊組件時(shí),n_clicks都會(huì)增加, 這個(gè)功能適用于dash_html_components庫(kù)里的所有組件。
在不同回調(diào)函數(shù)之間共享狀態(tài)
回調(diào)函數(shù)入門(mén)里提到過(guò)Dash的核心原則是絕對(duì)不要在變量范圍之外修改Dash回調(diào)函數(shù)的變量。修改任何全局變量都不安全。本章解釋這樣操作為什么不安全,并提出在回調(diào)函數(shù)間共享狀態(tài)的替代方式。
為什么要共享狀態(tài)?
某些應(yīng)用會(huì)有SQL查詢(xún)、運(yùn)行模擬或下載數(shù)據(jù)等擴(kuò)展性數(shù)據(jù)處理任務(wù),所以會(huì)使用多個(gè)回調(diào)函數(shù)。
與其讓每個(gè)回調(diào)函數(shù)都運(yùn)行同一個(gè)大規(guī)模運(yùn)算任務(wù),不如讓其中一個(gè)回調(diào)函數(shù)執(zhí)行任務(wù),然后將結(jié)果共享給其它回調(diào)函數(shù)。
為什么全局變量會(huì)破壞應(yīng)用
Dash的設(shè)計(jì)思路是實(shí)現(xiàn)在多用戶(hù)環(huán)境下,多人可以同時(shí)查看應(yīng)用,這就有了獨(dú)立會(huì)話(huà)的概念。
如果用戶(hù)可以修改應(yīng)用的全局變量,前一個(gè)用戶(hù)的會(huì)話(huà)就會(huì)重置全局變量,從而影響下一位用戶(hù)會(huì)話(huà)的值。
Dash的設(shè)計(jì)思路還包括運(yùn)行多個(gè)Python workers,以便多個(gè)回調(diào)函數(shù)能夠并行。這種情況一般使用gunicorn語(yǔ)法來(lái)實(shí)現(xiàn)。
$ gunicorn --workers 4 --threads 2 app:server
Dash應(yīng)用跨多個(gè)worker運(yùn)行時(shí),不會(huì)共享內(nèi)存,這意味著如果某個(gè)回調(diào)函數(shù)修改了全局變量,其改動(dòng)不會(huì)應(yīng)用于其它worker。
下面的例子展示了回調(diào)函數(shù)在其應(yīng)用范圍外修改數(shù)據(jù)。鑒于上述原因,它的運(yùn)行結(jié)果可能不靠譜。
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` 是變量在函數(shù)范圍之外的例子。
# 在回調(diào)中修改或重新分配這個(gè)變量不安全。
global df = df[df['c'] == value] # 不要這么干,不安全!
return len(df)
要修復(fù)這個(gè)問(wèn)題,只需為回調(diào)函數(shù)內(nèi)的新變量再指定一個(gè)篩選器即可,可以使用下面的方法。
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)
在回調(diào)函數(shù)之間共享數(shù)據(jù)
為了安全地跨多個(gè)python進(jìn)程共享數(shù)據(jù),需要將數(shù)據(jù)存儲(chǔ)在每個(gè)進(jìn)程都能訪問(wèn)的位置。 建議在這3個(gè)位置存儲(chǔ)數(shù)據(jù):
用戶(hù)瀏覽器會(huì)話(huà);
硬盤(pán)上,比如,文件或新建數(shù)據(jù)庫(kù);
像Redis一樣,存在共享內(nèi)存空間。
下面幾個(gè)例子詳細(xì)說(shuō)明了這三種方法。
例1 在Hidden Div中存儲(chǔ)數(shù)據(jù)
為了在用戶(hù)瀏覽器會(huì)話(huà)里保存數(shù)據(jù),需要:
- 通過(guò)https://community.plot.ly/t/sharing-a-dataframe-between-plots/6173里的方法,將數(shù)據(jù)保存為Dash前端的一部分;
- 將數(shù)據(jù)轉(zhuǎn)換為JSON文本格式,然后進(jìn)行存儲(chǔ)和傳輸;
- 以這種方式緩存的數(shù)據(jù)只在當(dāng)前用戶(hù)會(huì)話(huà)中生效;
- 打開(kāi)新的瀏覽器頁(yè)面后,回調(diào)函數(shù)用會(huì)計(jì)算數(shù)據(jù)。該數(shù)據(jù)僅在當(dāng)前會(huì)話(huà)的回調(diào)函數(shù)中緩存和傳輸;
- 與緩存不同,這種方法不會(huì)增加對(duì)內(nèi)存的占用;
- 網(wǎng)絡(luò)傳輸會(huì)產(chǎn)生成本。假如在回調(diào)函數(shù)之間共享10MB數(shù)據(jù),每次回調(diào)時(shí)都會(huì)通過(guò)網(wǎng)絡(luò)傳輸數(shù)據(jù)。
- 如果網(wǎng)絡(luò)成本太高,可以先做聚合計(jì)算再傳輸數(shù)據(jù)。 應(yīng)用一般不會(huì)顯示多于10MB的數(shù)據(jù),大部分情況下只顯示子集或子集的聚合結(jié)果。
本例概述了在回調(diào)函數(shù)中執(zhí)行大規(guī)模的數(shù)據(jù)處理步驟,以JSON格式進(jìn)行序列化輸出,并將其作為其他回調(diào)函數(shù)的輸入。本例使用標(biāo)準(zhǔn)Dash回調(diào)函數(shù),將JSON數(shù)據(jù)存儲(chǔ)在應(yīng)用的Hidden Div里。
global_df = pd.read_csv('...')
app.layout = html.Div([
dcc.Graph(id='graph'),
html.Table(id='table'),
dcc.Dropdown(id='dropdown'),
# 用于存儲(chǔ)中間值的Hidden Div。
html.Div(id='intermediate-value', style={'display': 'none'})
])
@app.callback(Output('intermediate-value', 'children'), [Input('dropdown', 'value')])
def clean_data(value):
# 清理大規(guī)模數(shù)據(jù)的步驟
cleaned_df = your_expensive_clean_or_compute_step(value)
# 通常使用下列語(yǔ)句
# 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):
# 通常使用下列語(yǔ)句
# 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 預(yù)聚合計(jì)算
如果數(shù)據(jù)量過(guò)大,即使通過(guò)網(wǎng)絡(luò)發(fā)送運(yùn)算后的數(shù)據(jù)代價(jià)也會(huì)很高。 在某些情況下,即便將數(shù)據(jù)序列化或使用JSON格式的運(yùn)算量也很大。
很多情況下,Dash應(yīng)用只顯示經(jīng)過(guò)計(jì)算、過(guò)濾的數(shù)據(jù)子集或聚合結(jié)果。 這樣就可以在處理回調(diào)時(shí),對(duì)數(shù)據(jù)進(jìn)行聚合預(yù)計(jì)算,將聚合結(jié)果傳輸給其它回調(diào)函數(shù)即可。
下面是將過(guò)濾或聚合過(guò)的數(shù)據(jù)傳輸給多個(gè)回調(diào)函數(shù)的例子。
@app.callback(
Output('intermediate-value', 'children'),
[Input('dropdown', 'value')])
def clean_data(value):
# 高消耗的查詢(xún)步驟
cleaned_df = your_expensive_clean_or_compute_step(value)
# 為了計(jì)算后期回調(diào)函數(shù)所需的數(shù)據(jù)而進(jìn)行的篩選
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)
本例說(shuō)明:
- 使用Flask-Cache插件在Redis中存儲(chǔ)全局變量。 通過(guò)函數(shù)訪問(wèn)數(shù)據(jù),通過(guò)該函數(shù)的輸入?yún)?shù)對(duì)輸出項(xiàng)進(jìn)行緩存與鍵入處理。
- 大規(guī)模運(yùn)算完成后,將Hidden Div里存儲(chǔ)的數(shù)據(jù)發(fā)送信令給其它回調(diào)函數(shù)。
- 注意,如果不用Redis,可以將數(shù)據(jù)保存至文件系統(tǒng)。詳細(xì)內(nèi)容請(qǐng)參閱:https://flask-caching.readthedocs.io/en/latest/。
- 因?yàn)樵试S大規(guī)模運(yùn)算占用一個(gè)進(jìn)程,所以使用信令這種方式?jīng)]什么問(wèn)題。如果不使用信令,每個(gè)回調(diào)函數(shù)都要進(jìn)行并行的大規(guī)模運(yùn)算,這樣鎖定的就不是1個(gè)進(jìn)程,而是4個(gè)進(jìn)程了。
這種方法的另一個(gè)優(yōu)點(diǎn)是,下一個(gè)會(huì)話(huà)可以使用預(yù)計(jì)算的值。如果輸入數(shù)量不多的話(huà),對(duì)應(yīng)用的運(yùn)行有很大好處。
下面是這個(gè)例子運(yùn)行后的示意圖。需要注意以下幾點(diǎn):
- 使用time.sleep(5)模擬大規(guī)模運(yùn)算進(jìn)程;
- 加載應(yīng)用時(shí),需要5秒渲染所有4副圖;
- 初始運(yùn)算僅阻斷1個(gè)進(jìn)程;
- 運(yùn)算完成后,發(fā)送信令,并行執(zhí)行4個(gè)回調(diào)函數(shù)渲染圖形。每個(gè)回調(diào)函數(shù)都從全局存儲(chǔ),即Redis的緩存中提取數(shù)據(jù);
- 在app.run里面設(shè)置processes = 6,即允許多個(gè)回調(diào)函數(shù)并行執(zhí)行。在生產(chǎn)環(huán)境中,使用
$ gunicorn --workers 6 --threads 2 app:server實(shí)現(xiàn)類(lèi)似的效果; - 如果之前已經(jīng)選擇過(guò),再在下拉菜單選擇值不會(huì)超過(guò)5秒,這是因?yàn)橐呀?jīng)預(yù)先從緩存中把備選值提取出來(lái)了;
- 與此類(lèi)似,重新加載頁(yè)面或在新窗口中打開(kāi)應(yīng)用也會(huì)比較快,這是因?yàn)槌跏紶顟B(tài)和初始的大規(guī)模運(yùn)算已經(jīng)執(zhí)行完畢了。

【呆鳥(niǎo)譯Py】Python交互式數(shù)據(jù)分析報(bào)告框架~Dash介紹
【呆鳥(niǎo)譯Py】Dash用戶(hù)指南01-02_安裝與應(yīng)用布局
【呆鳥(niǎo)譯Py】Dash用戶(hù)指南03_交互性簡(jiǎn)介
【呆鳥(niǎo)譯Py】Dash用戶(hù)指南04_交互式數(shù)據(jù)圖
【呆鳥(niǎo)譯Py】Dash用戶(hù)指南05_使用State進(jìn)行回調(diào)