dash第二章:dash的回調函數


在上一章中,我們了解到dash.layout描述了應用程序的外觀,是一個組件的層次結構樹。dash_html_components庫為所有html標記提供類,關鍵字參數來描述html屬性,如樣式、類名和id。dash_core_components庫生成更高級別的組件,如控件和圖形。
本章介紹如何使用回調函數(callback functions)制作Dash應用程序:當輸入組件的屬性發生更改時,Dash會自動調用這些函數,以更新另一個組件(輸出)中的某些屬性。
為了獲得最佳的用戶交互和圖表加載性能,生產DASH應用程序應該考慮Job Queue、HPC、DATASADER和DASH的水平縮放能力。
讓我們從一個交互式Dash應用程序的簡單示例開始。

一個簡單的Dash應用

運行以下代碼:

import dash
from dash import dcc
from dash import html
from dash.dependencies import Input, Output

app = dash.Dash(__name__)

app.layout = html.Div([
    html.H6("Change the value in the text box to see callbacks in action!"),
    html.Div([
        "Input: ",
        dcc.Input(id='my-input', value='initial value', type='text')
    ]),
    html.Br(),
    html.Div(id='my-output'),

])


@app.callback(
    Output(component_id='my-output', component_property='children'),
    Input(component_id='my-input', component_property='value')
)
# 我們下面定義的這個函數會自動被回填函數調用
def update_output_div(input_value):
    return 'Output: {}'.format(input_value)


if __name__ == '__main__':
    app.run_server(debug=True)

運行結果如下:
image

下面來解釋代碼的意思:

  1. 我們的app中的輸入和輸出被描述為@app.callback調試器中的參數
  2. 在本例中,我們的dcc.Input(id= ,value= ,type= )中,id='my-input'相當於這個輸入框的名字是my-inputvalue='initial value'指的是輸入框中的默認值是initial value,這也是為什么我們在回調函數中寫Input(component_id='my-input', component_property='value')的原因,component_id='my-input'表示回調函數中的輸入就是前面dcc.Input中的值。對於dcc.Output也是同理。
  3. 每當輸入屬性發生更改時,回調裝飾器包裝的函數將被自動調用。Dash為這個回調函數提供輸入屬性的新值作為其參數,Dash用函數返回的任何內容更新輸出組件的屬性。
  4. component_idcomponent_property關鍵字是可選的(每個對象只有兩個參數)。為了清晰起見,本示例中包含了這些內容,但為了簡潔易讀,本文檔的其余部分將省略這些內容。
  5. 千萬別把dash.dependencies.Inputdcc.Input搞混了。前者僅在這些回調定義中使用,后者是一個實際組件。
  6. 請注意,我們沒有在布局中為my_output組件的children屬性設置值。Dash應用程序啟動時,它會自動使用輸入組件的初始值調用所有回調,以填充輸出組件的初始狀態。在本例中,如果將div組件指定為html.Div(id='my-output',children='Hello world'),它將在應用程序啟動時被覆蓋,也就是說寫了等於白寫,還不如不寫。

這有點像用Microsoft Excel編程:每當一個單元格(輸入)發生變化時,依賴該單元格(輸出)的所有單元格都會自動更新。因為輸出會自動對輸入的變化做出反應,所以這被稱為“反應式編程”(Reactice Programming)。

還記得每個組件是如何完全通過一組關鍵字參數來描述的嗎?我們在Python中設置的這些參數成為組件的屬性,這些屬性現在很重要。借助Dash的交互性,我們可以使用回調動態更新這些屬性中的任何一個。我們通常會更新HTML組件的children屬性以顯示新文本(記住,children組件負責組件的內容)或dcc.Graphfigure屬性以顯示新數據。我們還可以更新組件的style,甚至更新dcc.Dropdown中的可用選項!

讓我們來看看dcc.Slider如何更新dcc.Graph的吧?

帶數字和滑塊的Dash應用程序布局

import dash
from dash import dcc
from dash import html
import plotly.express as px
import pandas as pd
from dash.dependencies import Input, Output

df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/gapminderDataFiveYear.csv')

app = dash.Dash(__name__)

app.layout = html.Div([
    dcc.Graph(id='graph-with-slider'),
    dcc.Slider(
        id='year-slider',
        min=df['year'].min(),
        max=df['year'].max(),
        value=df['year'].min(),
        marks={str(year): str(year) for year in df['year'].unique()},
        step=None
    )
])


@app.callback(
    Output('graph-with-slider', 'figure'),
    Input('year-slider', 'value'))
def update_figure(selected_year):
    filtered_df = df[df.year == selected_year]

    fig = px.scatter(filtered_df, x="gdpPercap", y="lifeExp",
                     size="pop", color="continent", hover_name="country",
                     log_x=True, size_max=55)

    fig.update_layout(transition_duration=500)

    return fig


if __name__ == '__main__':
    app.run_server(debug=True)

注:代碼中的csv文件需要FQ才能下載

運行結果如下:
image

在本例中,dcc.Slidervalue屬性是這個app的輸入,dcc.Grahpfigure屬性是這個app的輸出。無論何時dcc.Slidervalue被改變,Dash都會調用update_figure這個回調函數來實時更新圖像。這個函數會根據滑塊的值自動過濾DataFrame中的數據,然后將處理過的數據返回給figure,畫出新的圖像。

這里有幾點值得注意的地方:

  1. 我們再此代碼的最開頭用Pandas這個庫來加載數據:df = pd.read_csv('''')。這個dataframe(df)是一個全局變量,能夠被回調函數調用。
  2. 將數據加載到內存可能會很昂貴。通過在應用程序開始時而不是在回調函數中加載查詢數據,我們可以確保此操作只在應用程序服務器啟動時執行一次。當用戶訪問應用程序或與應用程序交互時,該數據(df)已經存在於內存中。如果可能的話,昂貴的初始化(如下載或查詢數據)應該在應用程序的全局范圍內完成,而不是在回調函數中完成。
  3. 回調不會修改原始數據,它只通過使用pandas進行過濾來創建dataframe的副本。這一點很重要:回調絕不能修改其范圍之外的變量。如果回調修改了全局狀態,那么一個用戶的會話可能會影響下一個用戶的會話,並且當應用部署在多個進程或線程上時,這些修改不會在會話之間共享。
  4. 我們正在使用layout.transition來像動畫一樣對數據進行隨時間而做平滑的改變。

具有多個輸入的Dash應用程序

在Dash中,任何輸出都可以有多個輸入組件。下面是一個簡單的示例,它將五個輸入(兩個dcc.Dropdown組件、兩個dcc.radiotems組件和一個dcc.Slider組件的value屬性)綁定到一個輸出組件(dcc.Graph組件的figure屬性)。注意這個應用程序是如何運行的。callback如何在輸出之后列出所有五個輸入項。

import dash
from dash import dcc
from dash import html
from dash.dependencies import Input, Output
import plotly.express as px

import pandas as pd

app = dash.Dash(__name__)

df = pd.read_csv('https://plotly.github.io/datasets/country_indicators.csv')

available_indicators = df['Indicator Name'].unique()

app.layout = html.Div([
    html.Div([

        html.Div([
            dcc.Dropdown(
                id='xaxis-column',
                options=[{'label': i, 'value': i} for i in available_indicators],
                value='Fertility rate, total (births per woman)'
            ),
            dcc.RadioItems(
                id='xaxis-type',
                options=[{'label': i, 'value': i} for i in ['Linear', 'Log']],
                value='Linear',
                labelStyle={'display': 'inline-block'}
            )
        ], style={'width': '48%', 'display': 'inline-block'}),

        html.Div([
            dcc.Dropdown(
                id='yaxis-column',
                options=[{'label': i, 'value': i} for i in available_indicators],
                value='Life expectancy at birth, total (years)'
            ),
            dcc.RadioItems(
                id='yaxis-type',
                options=[{'label': i, 'value': i} for i in ['Linear', 'Log']],
                value='Linear',
                labelStyle={'display': 'inline-block'}
            )
        ], style={'width': '48%', 'float': 'right', 'display': 'inline-block'})
    ]),

    dcc.Graph(id='indicator-graphic'),

    dcc.Slider(
        id='year--slider',
        min=df['Year'].min(),
        max=df['Year'].max(),
        value=df['Year'].max(),
        marks={str(year): str(year) for year in df['Year'].unique()},
        step=None
    )
])


@app.callback(
    Output('indicator-graphic', 'figure'),
    Input('xaxis-column', 'value'),
    Input('yaxis-column', 'value'),
    Input('xaxis-type', 'value'),
    Input('yaxis-type', 'value'),
    Input('year--slider', 'value'))
def update_graph(xaxis_column_name, yaxis_column_name,
                 xaxis_type, yaxis_type,
                 year_value):
    dff = df[df['Year'] == year_value]

    fig = px.scatter(x=dff[dff['Indicator Name'] == xaxis_column_name]['Value'],
                     y=dff[dff['Indicator Name'] == yaxis_column_name]['Value'],
                     hover_name=dff[dff['Indicator Name'] == yaxis_column_name]['Country Name'])

    fig.update_layout(margin={'l': 40, 'b': 40, 't': 10, 'r': 0}, hovermode='closest')

    fig.update_xaxes(title=xaxis_column_name,
                     type='linear' if xaxis_type == 'Linear' else 'log')

    fig.update_yaxes(title=yaxis_column_name,
                     type='linear' if yaxis_type == 'Linear' else 'log')

    return fig


if __name__ == '__main__':
    app.run_server(debug=True)

輸出如下:
image

在這個例子中,只要dcc.Dropdown,dcc.Sliderdcc.RadioItems中的任意一個改變,圖像就會發生對應的變化!

回調的輸入參數是每個“輸入”屬性的當前值,按指定的順序排列。

即使一次只更改一個輸入(即,用戶在給定時刻只能更改一個下拉列表的值),Dash也會收集所有指定輸入屬性的當前狀態,並將它們傳遞給回調函數。這些回調函數始終保證接收應用程序的更新狀態。

讓我們擴展我們的示例以包括多個輸出。

具有多個輸出的Dash應用程序

到目前為止,我們編寫的所有回調只更新一個輸出屬性。我們還可以一次更新多個輸出:列出所有要在應用程序中更新的屬性。回調,並從回調中返回多項。如果兩個輸出依賴於相同的計算密集型中間結果(意思就是如果兩個輸出的計算相同且很復雜,那么這么做就能減少計算量,節省時間),例如緩慢的數據庫查詢,這一點尤其有用。
復制並運行下面代碼:

import dash
from dash import dcc
from dash import html
from dash.dependencies import Input, Output

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

app.layout = html.Div([
    dcc.Input(
        id='num-multi',
        type='number',
        value=5
    ),
    html.Table([
        html.Tr([html.Td(['x', html.Sup(2)]), html.Td(id='square')]),
        html.Tr([html.Td(['x', html.Sup(3)]), html.Td(id='cube')]),
        html.Tr([html.Td([2, html.Sup('x')]), html.Td(id='twos')]),
        html.Tr([html.Td([3, html.Sup('x')]), html.Td(id='threes')]),
        html.Tr([html.Td(['x', html.Sup('x')]), html.Td(id='x^x')]),
    ]),
])


@app.callback(
    Output('square', 'children'),
    Output('cube', 'children'),
    Output('twos', 'children'),
    Output('threes', 'children'),
    Output('x^x', 'children'),
    Input('num-multi', 'value'))
def callback_a(x):
    return x**2, x**3, 2**x, 3**x, x**x


if __name__ == '__main__':
    app.run_server(debug=True)

首先讓我來對上述代碼做一個簡單的說明:html.Tabel()是創建一個表格,里面的值是一個列表,列表中元素是html.Tr(),每個html.Tr()代表一行,而html.Td()代表的是一個格子。

代碼的運行結果如下:
image

警告:合並輸出並不總是一個好主意:

  • 如果輸出依賴於相同輸入的部分(而不是全部),那么將它們分開可以避免不必要的更新。
  • 如果輸出具有相同的輸入,但它們使用這些輸入執行非常不同的計算,保持回調分離可以允許它們並行運行。

帶鏈接回調的Dash應用程序

您還可以將輸出和輸入鏈接在一起:一個回調函數的輸出可以作為另一個回調函數的輸入。
此模式可用於創建動態UI,例如,一個輸入組件更新另一個輸入組件的可用選項。下面是一個簡單的例子。
復制並運行下面的代碼:

# -*- coding: utf-8 -*-
import dash
from dash import dcc
from dash import html
from dash.dependencies import Input, Output

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

all_options = {
    'America': ['New York City', 'San Francisco', 'Cincinnati'],
    'Canada': [u'Montréal', 'Toronto', 'Ottawa']
}
app.layout = html.Div([
    dcc.RadioItems(
        id='countries-radio',
        options=[{'label': k, 'value': k} for k in all_options.keys()],
        value='America'
    ),

    html.Hr(),

    dcc.RadioItems(id='cities-radio'),

    html.Hr(),

    html.Div(id='display-selected-values')
])

#選擇不同的國家后,下面的表會顯示對應國家的城市
@app.callback(
    Output('cities-radio', 'options'),
    Input('countries-radio', 'value'))
def set_cities_options(selected_country):
    return [{'label': i, 'value': i} for i in all_options[selected_country]]

#當選擇新的國家后,需要設置一個默認城市,所以統一設置為第一個城市
@app.callback(
    Output('cities-radio', 'value'),
    Input('cities-radio', 'options'))
def set_cities_value(available_options):
    return available_options[0]['value']

#依據國家和城市輸出一句話
@app.callback(
    Output('display-selected-values', 'children'),
    Input('countries-radio', 'value'),
    Input('cities-radio', 'value'))
def set_display_children(selected_country, selected_city):
    return u'{} is a city in {}'.format(
        selected_city, selected_country,
    )


if __name__ == '__main__':
    app.run_server(debug=True)

基於第一個dcc.RadioItems的選項,第一個回調更新第二個dcc.RadioItems中的可用選項。

options屬性更改時,第二個回調將設置初始值:它將其設置為該選項數組中的第一個值。

最后一個回調顯示每個組件的選定值。如果更改國家/地區dcc.RadioItems的值,Dash將等到城市組件的值更新后再調用最終回調。這可以防止您的回調被稱為不一致的狀態,如“American”和“Montreal”。

帶狀態的Dash應用程序

在某些情況下,應用程序中可能會有一個類似於“表單”的模式。在這種情況下,你可能希望程序在用戶徹底輸入完信息后再進行運算,而不是用戶一改變其中的部分信息,程序就馬上更新。
將回調直接附加到輸入值可以如下所示:

# -*- coding: utf-8 -*-
import dash
from dash import dcc
from dash import html
from dash.dependencies import Input, Output

external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"]

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

app.layout = html.Div([
    dcc.Input(id="input-1", type="text", value="Montréal"),
    dcc.Input(id="input-2", type="text", value="Canada"),
    html.Div(id="number-output"),
])


@app.callback(
    Output("number-output", "children"),
    Input("input-1", "value"),
    Input("input-2", "value"),
)
def update_output(input1, input2):
    return u'Input 1 is "{}" and Input 2 is "{}"'.format(input1, input2)


if __name__ == "__main__":
    app.run_server(debug=True)

在本例中,只要輸入描述的任何屬性發生更改,就會觸發回調函數。State解決了個問題,它能夠允許你輸入額外值而不調動回調函數!這里有一個和前面相似的程序,但是兩個dcc.Input組件擁有State和新的按鈕。說人話就是在輸入的后面加了一個啟動回調函數的按鈕。

復制並運行下面的代碼:

# -*- coding: utf-8 -*-
import dash
from dash import dcc
from dash import html
from dash.dependencies import Input, Output, State
import dash_design_kit as ddk

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

app.layout = html.Div([
    dcc.Input(id='input-1-state', type='text', value='Montréal'),
    dcc.Input(id='input-2-state', type='text', value='Canada'),
    html.Button(id='submit-button-state', n_clicks=0, children='Submit'),
    html.Div(id='output-state')
])


@app.callback(Output('output-state', 'children'),
              Input('submit-button-state', 'n_clicks'),
              State('input-1-state', 'value'),
              State('input-2-state', 'value'))
def update_output(n_clicks, input1, input2):
    return u'''
        The Button has been pressed {} times,
        Input 1 is "{}",
        and Input 2 is "{}"
    '''.format(n_clicks, input1, input2)


if __name__ == '__main__':
    app.run_server(debug=True)

代碼的運行結果如下:
image
你可以嘗試一下改變輸入框里的值,你會發現只有點擊SUBMIT時下面的值才會改變,這就是State的效果。

在本例中,更改dcc.Input()中的文本不會觸發回調,但單擊按鈕會觸發回調。即使它們不會觸發回調函數本身,但是dcc.Input當前的值仍然會傳遞到回調函數中。
注意,我們通過監聽htmln_clicks屬性來判斷是否觸發回調。n_clicks是一個屬性,它在每次單擊組件時都會遞增。dash_html_components 庫中的每個組件都可以使用它,但最好和按鈕配合使用。

本章總結

我們已經在Dash中介紹了回調的基本原理。Dash應用程序基於一組簡單但功能強大的原則構建:用戶界面可通過反應式回調進行定制。組件的每個屬性/屬性都可以作為回調的輸出進行修改,而屬性的子集(例如dcc.Dropdown組件的value屬性)可以由用戶通過與頁面交互進行編輯。

下一章我們將學習如何將回調函數應用到畫圖中。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM