1. 程式人生 > >(資料科學學習手札106)Python+Dash快速web應用開發——回撥互動篇(下)

(資料科學學習手札106)Python+Dash快速web應用開發——回撥互動篇(下)

> 本文示例程式碼已上傳至我的`Github`倉庫[https://github.com/CNFeffery/DataScienceStudyNotes](https://github.com/CNFeffery/DataScienceStudyNotes) # 1 簡介    這是我的系列教程**Python+Dash快速web應用開發**的第五期,在上一期的文章中,我們針對`Dash`中有關回調的一些技巧性的特性進行了介紹,使得我們可以更愉快地為`Dash`應用編寫回調互動功能。   而今天的文章作為**回撥互動**系統性內容的最後一期,我將帶大家get一些`Dash`中實際應用效果驚人的**高階回撥特性**,繫好安全帶,我們起飛~
圖1
# 2 Dash中的高階回撥特性 ## 2.1 控制部分回撥輸出不更新   在很多應用場景下,我們給某個回撥函式綁定了多個`Output()`,這時如果這些`Output()`並不是每次觸發回撥都需要被更新,那麼就可以根據`Input()`值的不同,來配合`dash.no_update`作為對應`Output()`的返回值,從而實現部分`Output()`不更新,譬如下面的例子: > app1.py ```Python 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) ```
圖2
  可以觀察到,我們根據`n_clicks`數值的不同,在對應各個`Output()`返回值中對符合條件的部件進行更新,其他的都用`dash.no_update`來代替,從而實現了局部更新,非常實用且簡單。 ## 2.2 基於模式匹配的回撥   這是`Dash`在1.11.0版本開始引入的新特性,它所實現的功能是將多個部件繫結組織在同一個`id`屬性下,這聽起來有一點抽象,我們先從一個形象的例子來出發:   假如我們要開發一個簡單的**記賬**應用,它通過第一排若干`Input()`部件及一個`Button()`部件來記錄並提交每筆賬對應的相關資訊,並且在最下方輸出已記錄賬目金額之和: >
app2.py ```Python 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) ```
圖3
  上面這個應用中,體現出的**模式匹配**內容即為開頭從`dash.dependencies`引入的`ALL`,它是`Dash`**模式匹配**中的一種模式,而我們在回撥函式`update_account_records()`中為已有記賬記錄追加新紀錄時,使用到: ```Python # 以字典形式定義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 ```Python 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) ```
圖4
  可以看到,在`refresh_code_output()`前應用`MATCH`模式匹配後,我們點選某個部件時,只有跟它`index`匹配的部件才會打印出相對應的輸出,非常的方便~ ## 2.3 多輸入情況下獲取部件觸發情況   在很多應用場景下,我們的某個回撥可能擁有多個`Input`輸入,但學過前面的內容我們已經清楚,不管有幾個`Input`,只要其中有一個部件其輸入屬性發生變化,都會觸發本輪迴調,但是如果我們就想知道究竟是**哪個**`Input`觸發了本輪迴調該怎麼辦呢?   這在`Dash`中可以通過`dash.callback_context`來方便的實現,它只能在回撥函式中被執行,從而獲取回撥過程的諸多上下文資訊,先從下面這個簡單的例子出發看看`dash.callback_context`到底給我們帶來了哪些有價值的資訊: > app4.py ```Python 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) ```
圖5
  可以看到,我們安插在回撥函式裡的`dash.callback_context`幫我們記錄了從訪問`Dash`開始,到最近一次執行回撥期間,對應回撥的輸入輸出資訊變化情況、最近一次觸發資訊,非常的實用,可以支撐起很多複雜應用場景。 ## 2.4 在瀏覽器端執行回撥過程   `Dash`雖然很方便,使得我們可以完全不用書寫`js`程式碼就可以實現各種回撥互動,但把所有的互動響應計算過程都交給服務端來做,省事倒是很省事,但會給伺服器帶來不小的計算和網路傳輸壓力。   因此很多容易頻繁觸發且與主要的數值計算無關的互動行為,完全可以搬到瀏覽器端執行,既快速又不吃伺服器的計算資源,這也是當初`JavaScript`被髮明的一個重要原因,而在`Dash`中,也為略懂`js`的使用者提供了在瀏覽器端執行一些回撥的貼心功能。   從一個很簡單的點選按鈕,實現部分網頁內容的開啟與關閉出發,這裡我們提前使用到`dbc.Collapse`部件,用於將所包含的網頁內容與其它按鈕部件的點選行為進行繫結: > app5.py ```Python 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`的邏輯反值,從而實現了摺疊內容的開啟與關閉切換: ```javascript function(n_clicks, is_open) { return !is_open; } ```   便實現了瀏覽器端回撥!
圖6
  而如果你想要執行的瀏覽器端`js`回撥函式程式碼有點長,還可以按照下圖格式,把你的大段`js`回撥函式程式碼放置於`assets`目錄下對應路徑裡的`js`指令碼中:
圖7
  接著再在`dash`中按照下列格式編寫關聯輸入輸出與上述`js`回撥的簡短語句即可: ```Python app.clientside_callback( ClientsideFunction( namespace='名稱空間名稱', function_name='對應js回撥函式名' ), ''' 按順序組織你的Output、Input以及State... ... ''' ) ```   下面我們直接以大家喜聞樂見的資料視覺化頂級框架`echarts`為例,來寫一個根據不同輸入值切換渲染出的圖表型別,**注意**請從官網把依賴的`echarts.min.js`下載到我們的`assets`路徑下對應位置,它會在我們的`Dash`應用啟動時與所有`assets`下的資源一起自動被載入到瀏覽器中: > app6.py ```Python 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) ```
圖8
  效果十分驚人,從此我們使用`Dash`不僅僅可以使用`Python`生態的工具,還可以配合對前端內容支援更好的`js`,起飛! ---   至此我們的`Dash`回撥互動三部曲已結束,接下來的文章我將開始帶大家遨遊豐富的各種`Dash`前端部件,涵蓋了網頁部件、資料視覺化圖表以及地圖視覺化等內容,敬請期待這場奇妙之旅吧~   以上就是本文的全部內容,歡迎在評論區與我進行