本文示例程式碼已上傳至我的
Github
倉庫https://github.com/CNFeffery/DataScienceStudyNotes
1 簡介
這是我的系列教程Python+Dash快速web應用開發的第五期,在上一期的文章中,我們針對Dash
中有關回撥的一些技巧性的特性進行了介紹,使得我們可以更愉快地為Dash
應用編寫回撥互動功能。
而今天的文章作為回撥互動系統性內容的最後一期,我將帶大家get一些Dash
中實際應用效果驚人的高階回撥特性,繫好安全帶,我們起飛~
2 Dash中的高階回撥特性
2.1 控制部分回撥輸出不更新
在很多應用場景下,我們給某個回撥函式繫結了多個Output()
,這時如果這些Output()
並不是每次觸發回撥都需要被更新,那麼就可以根據Input()
值的不同,來配合dash.no_update
作為對應Output()
的返回值,從而實現部分Output()
不更新,譬如下面的例子:
app1.py
import dash
import dash_bootstrap_components as dbc
import dash_html_components as html
from dash.dependencies import Input, Output
import time
app = dash.Dash(__name__)
app.layout = html.Div(
dbc.Container(
[
html.Br(),
html.Br(),
html.Br(),
dbc.Row(
dbc.Col(
dbc.Button('按鈕',
color='primary',
id='button',
n_clicks=0)
)
),
html.Br(),
dbc.Row(
[
dbc.Col('尚未觸發', id='record-1'),
dbc.Col('尚未觸發', id='record-2'),
dbc.Col('尚未觸發', id='record-n')
]
)
]
)
)
@app.callback(
[Output('record-1', 'children'),
Output('record-2', 'children'),
Output('record-n', 'children'),
],
Input('button', 'n_clicks'),
prevent_initial_call=True
)
def record_click_event(n_clicks):
if n_clicks == 1:
return (
'第1次點選:{}'.format(time.strftime('%H:%M:%S', time.localtime(time.time()))),
dash.no_update,
dash.no_update
)
elif n_clicks == 2:
return (
dash.no_update,
'第2次點選:{}'.format(time.strftime('%H:%M:%S', time.localtime(time.time()))),
dash.no_update
)
elif n_clicks >= 3:
return (
dash.no_update,
dash.no_update,
'第3次及以上點選:{}'.format(time.strftime('%H:%M:%S', time.localtime(time.time()))),
)
if __name__ == '__main__':
app.run_server(debug=True)
可以觀察到,我們根據n_clicks
數值的不同,在對應各個Output()
返回值中對符合條件的部件進行更新,其他的都用dash.no_update
來代替,從而實現了區域性更新,非常實用且簡單。
2.2 基於模式匹配的回撥
這是Dash
在1.11.0版本開始引入的新特性,它所實現的功能是將多個部件繫結組織在同一個id
屬性下,這聽起來有一點抽象,我們先從一個形象的例子來出發:
假如我們要開發一個簡單的記賬應用,它通過第一排若干Input()
部件及一個Button()
部件來記錄並提交每筆賬對應的相關資訊,並且在最下方輸出已記錄賬目金額之和:
app2.py
import dash
import dash_bootstrap_components as dbc
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State, ALL
import re
app = dash.Dash(__name__)
app.layout = html.Div(
[
html.Br(),
html.Br(),
dbc.Container(
dbc.Row(
[
dbc.Col(
dbc.InputGroup(
[
dbc.InputGroupAddon("金額", addon_type="prepend"),
dbc.Input(
id='account-amount',
placeholder='請輸入金額',
type="number",
),
dbc.InputGroupAddon("元", addon_type="append"),
],
),
width=5
),
dbc.Col(
dcc.Dropdown(
id='account-type',
options=[
{'label': '生活開銷', 'value': '生活開銷'},
{'label': '人情往來', 'value': '人情往來'},
{'label': '醫療保健', 'value': '醫療保健'},
{'label': '旅遊休閒', 'value': '旅遊休閒'},
],
placeholder='請選擇型別:'
),
width=5
),
dbc.Col(
dbc.Button('提交記錄', id='account-submit'),
width=2
)
]
)
),
html.Br(),
dbc.Container([], id='account-record-container'),
dbc.Container('暫無記錄!', id='account-record-sum')
]
)
@app.callback(
Output('account-record-container', 'children'),
Input('account-submit', 'n_clicks'),
[State('account-record-container', 'children'),
State('account-amount', 'value'),
State('account-type', 'value')],
prevent_initial_call=True
)
def update_account_records(n_clicks, children, account_amount, account_type):
'''
用於處理每一次的記賬輸入並渲染前端記錄
'''
if account_amount and account_type:
children.append(dbc.Row(
dbc.Col(
'【{}】類開銷【{}】元'.format(account_type, account_amount)
),
# 以字典形式定義id
id={'type': 'single-account_record', 'index': children.__len__()}
))
return children
@app.callback(
Output('account-record-sum', 'children'),
Input({'type': 'single-account_record', 'index': ALL}, 'children'),
prevent_initial_call=True
)
def refresh_account_sum(children):
'''
對多部件集合single-account_record下所有賬目記錄進行求和
'''
return '賬本總開銷:{}'.format(sum([int(re.findall('\d+',
child['props']['children'])[0])
for child in children]))
if __name__ == '__main__':
app.run_server(debug=True)
上面這個應用中,體現出的模式匹配內容即為開頭從dash.dependencies
引入的ALL
,它是Dash
模式匹配中的一種模式,而我們在回撥函式update_account_records()
中為已有記賬記錄追加新紀錄時,使用到:
# 以字典形式定義id
id={'type': 'single-account_record', 'index': children.__len__()}
這裡不同於以前我們採取的id=某個字串
的定義方法,換成字典之後,其type
鍵值對用來記錄唯一id
資訊,每一次新紀錄追加時type
值都相等,因為它們被組織為同id部件集合,而鍵值對index
則用於在type
值相同的一個部件集合下,區分出不同的獨立部件元素。
因為將傳統的唯一id部件替換成同id部件集合,所以我們後面的回撥函式refresh_account_sum()
的輸入元素只需要定義單個Input()
即可,再在函式內部按照不同的index
值取出需要的集合內各成員記錄值,非常便於我們書寫出簡練清爽的Dash
程式碼,便於之後進一步的修改與重構。
你可以通過最下面列印出的每次refresh_account_sum()
所接收到的children
引數json
格式結果來弄清我是如何在return
值的地方取出歷史記賬金額並計算的。
而除了上面介紹的一股腦返回所有集合內成員部件的ALL
模式之外,還有另一種更有針對性的MATCH
模式,它應用於結合內成員部件可互動輸入值的情況,譬如下面這個簡單的例子,我們定義一個簡單的用於查詢省份行政程式碼的應用,配合MATCH
模式來實現彼此成對獨立輸出:
app3.py
import dash
import dash_bootstrap_components as dbc
import dash_html_components as html
from dash.dependencies import Input, Output, State, MATCH
import dash_core_components as dcc
app = dash.Dash(__name__)
app.layout = html.Div(
[
html.Br(),
html.Br(),
html.Br(),
dbc.Container(
[
dbc.Row(
dbc.Col(
dbc.Button('新增查詢', id='add-item', outline=True)
)
),
html.Hr()
]
),
dbc.Container([], id='query-container')
]
)
region2code = {
'北京市': '110000000000',
'重慶市': '500000000000',
'安徽省': '340000000000'
}
@app.callback(
Output('query-container', 'children'),
Input('add-item', 'n_clicks'),
State('query-container', 'children'),
prevent_initial_call=True
)
def add_query_item(n_clicks, children):
children.append(
dbc.Row(
[
dbc.Col(
[
# 生成index相同的dropdown部件與文字輸出部件
dcc.Dropdown(id={'type': 'select-province', 'index': children.__len__()},
options=[{'label': label, 'value': label} for label in region2code.keys()],
placeholder='選擇省份:'),
html.P('請輸入要查詢的省份!', id={'type': 'code-output', 'index': children.__len__()})
]
)
]
)
)
return children
@app.callback(
Output({'type': 'code-output', 'index': MATCH}, 'children'),
Input({'type': 'select-province', 'index': MATCH}, 'value')
)
def refresh_code_output(value):
if value:
return region2code[value]
else:
return dash.no_update
if __name__ == '__main__':
app.run_server(debug=True)
可以看到,在refresh_code_output()
前應用MATCH
模式匹配後,我們點選某個部件時,只有跟它index
匹配的部件才會列印出相對應的輸出,非常的方便~
2.3 多輸入情況下獲取部件觸發情況
在很多應用場景下,我們的某個回撥可能擁有多個Input
輸入,但學過前面的內容我們已經清楚,不管有幾個Input
,只要其中有一個部件其輸入屬性發生變化,都會觸發本輪迴調,但是如果我們就想知道究竟是哪個Input
觸發了本輪迴調該怎麼辦呢?
這在Dash
中可以通過dash.callback_context
來方便的實現,它只能在回撥函式中被執行,從而獲取回撥過程的諸多上下文資訊,先從下面這個簡單的例子出發看看dash.callback_context
到底給我們帶來了哪些有價值的資訊:
app4.py
import dash
import dash_html_components as html
import dash_bootstrap_components as dbc
from dash.dependencies import Input, Output
import json
app = dash.Dash(__name__)
app.layout = html.Div(
dbc.Container(
[
html.Br(),
html.Br(),
html.Br(),
dbc.Row(
[
dbc.Col(dbc.Button('A', id='A', n_clicks=0)),
dbc.Col(dbc.Button('B', id='B', n_clicks=0)),
dbc.Col(dbc.Button('C', id='C', n_clicks=0))
]
),
dbc.Row(
[
dbc.Col(html.P('按鈕A未點選', id='A-output')),
dbc.Col(html.P('按鈕B未點選', id='B-output')),
dbc.Col(html.P('按鈕C未點選', id='C-output'))
]
),
dbc.Row(
dbc.Col(
html.Pre(id='raw-json')
)
)
]
)
)
@app.callback(
[Output('A-output', 'children'),
Output('B-output', 'children'),
Output('C-output', 'children'),
Output('raw-json', 'children')],
[Input('A', 'n_clicks'),
Input('B', 'n_clicks'),
Input('C', 'n_clicks')],
prevent_initial_call=True
)
def refresh_output(A_n_clicks, B_n_clicks, C_n_clicks):
# 獲取本輪迴調狀態下的上下文資訊
ctx = dash.callback_context
# 取出對應State、最近一次觸發部件以及Input資訊
ctx_msg = json.dumps({
'states': ctx.states,
'triggered': ctx.triggered,
'inputs': ctx.inputs
}, indent=2)
return A_n_clicks, B_n_clicks, C_n_clicks, ctx_msg
if __name__ == '__main__':
app.run_server(debug=True)
可以看到,我們安插在回撥函式裡的dash.callback_context
幫我們記錄了從訪問Dash
開始,到最近一次執行回撥期間,對應回撥的輸入輸出資訊變化情況、最近一次觸發資訊,非常的實用,可以支撐起很多複雜應用場景。
2.4 在瀏覽器端執行回撥過程
Dash
雖然很方便,使得我們可以完全不用書寫js
程式碼就可以實現各種回撥互動,但把所有的互動響應計算過程都交給服務端來做,省事倒是很省事,但會給伺服器帶來不小的計算和網路傳輸壓力。
因此很多容易頻繁觸發且與主要的數值計算無關的互動行為,完全可以搬到瀏覽器端執行,既快速又不吃伺服器的計算資源,這也是當初JavaScript
被發明的一個重要原因,而在Dash
中,也為略懂js
的使用者提供了在瀏覽器端執行一些回撥的貼心功能。
從一個很簡單的點選按鈕,實現部分網頁內容的開啟與關閉出發,這裡我們提前使用到dbc.Collapse
部件,用於將所包含的網頁內容與其它按鈕部件的點選行為進行繫結:
app5.py
import dash
import dash_bootstrap_components as dbc
import dash_html_components as html
from dash.dependencies import Input, Output, State
app = dash.Dash(__name__)
app.layout = html.Div(
dbc.Container(
[
html.Br(),
html.Br(),
html.Br(),
dbc.Button('服務端回撥', id='server-button'),
dbc.Collapse('服務端摺疊內容', id='server-collapse'),
html.Hr(),
dbc.Button('瀏覽器端回撥', id='browser-button'),
dbc.Collapse('瀏覽器端摺疊內容', id='browser-collapse'),
]
)
)
@app.callback(
Output('server-collapse', 'is_open'),
Input('server-button', 'n_clicks'),
State('server-collapse', 'is_open'),
prevent_initial_call=True
)
def server_callback(n_clicks, is_open):
return not is_open
# 在dash中定義瀏覽器端回撥函式的特殊格式
app.clientside_callback(
"""
function(n_clicks, is_open) {
return !is_open;
}
""",
Output('browser-collapse', 'is_open'),
Input('browser-button', 'n_clicks'),
State('browser-collapse', 'is_open'),
prevent_initial_call=True
)
if __name__ == '__main__':
app.run_server(debug=True)
可以看到,服務端回撥我們照常寫,而瀏覽器端回撥通過傳入一個非常簡單的js
函式,在每次回撥時接受輸入並輸出is_open
的邏輯反值,從而實現了摺疊內容的開啟與關閉切換:
function(n_clicks, is_open) {
return !is_open;
}
便實現了瀏覽器端回撥!
而如果你想要執行的瀏覽器端js
回撥函式程式碼有點長,還可以按照下圖格式,把你的大段js
回撥函式程式碼放置於assets
目錄下對應路徑裡的js
指令碼中:
接著再在dash
中按照下列格式編寫關聯輸入輸出與上述js
回撥的簡短語句即可:
app.clientside_callback(
ClientsideFunction(
namespace='名稱空間名稱',
function_name='對應js回撥函式名'
),
'''
按順序組織你的Output、Input以及State... ...
'''
)
下面我們直接以大家喜聞樂見的資料視覺化頂級框架echarts
為例,來寫一個根據不同輸入值切換渲染出的圖表型別,注意請從官網把依賴的echarts.min.js
下載到我們的assets
路徑下對應位置,它會在我們的Dash
應用啟動時與所有assets
下的資源一起自動被載入到瀏覽器中:
app6.py
import dash
import dash_bootstrap_components as dbc
import dash_html_components as html
import dash_core_components as dcc
from dash.dependencies import Input, Output, ClientsideFunction
app = dash.Dash(__name__)
# 編寫一個根據dropdown不同輸入值切換對應圖表型別的小應用
app.layout = html.Div(
dbc.Container(
[
html.Br(),
dbc.Row(
dbc.Col(
dcc.Dropdown(
id='chart-type',
options=[
{'label': '折線圖', 'value': '折線圖'},
{'label': '堆積面積圖', 'value': '堆積面積圖'},
],
value='折線圖'
),
width=3
)
),
html.Br(),
dbc.Row(
dbc.Col(
html.Div(
html.Div(
id='main',
style={
'height': '100%',
'width': '100%'
}
),
style={
'width': '800px',
'height': '500px'
}
)
)
)
]
)
)
app.clientside_callback(
# 關聯自編js指令碼中的相應回撥函式
ClientsideFunction(
namespace='clientside',
function_name='switch_chart'
),
Output('main', 'children'),
Input('chart-type', 'value')
)
if __name__ == '__main__':
app.run_server(debug=True)
效果十分驚人,從此我們使用Dash
不僅僅可以使用Python
生態的工具,還可以配合對前端內容支援更好的js
,起飛!
至此我們的Dash
回撥互動三部曲已結束,接下來的文章我將開始帶大家遨遊豐富的各種Dash
前端部件,涵蓋了網頁部件、資料視覺化圖表以及地圖視覺化等內容,敬請期待這場奇妙之旅吧~
以上就是本文的全部內容,歡迎在評論區與我進行討論。