在上一章中,我们了解到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)
运行结果如下:

下面来解释代码的意思:
- 我们的app中的输入和输出被描述为
@app.callback调试器中的参数- 在本例中,我们的
dcc.Input(id= ,value= ,type= )中,id='my-input'相当于这个输入框的名字是my-input,value='initial value'指的是输入框中的默认值是initial value,这也是为什么我们在回调函数中写Input(component_id='my-input', component_property='value')的原因,component_id='my-input'表示回调函数中的输入就是前面dcc.Input中的值。对于dcc.Output也是同理。- 每当输入属性发生更改时,回调装饰器包装的函数将被自动调用。Dash为这个回调函数提供输入属性的新值作为其参数,Dash用函数返回的任何内容更新输出组件的属性。
component_id和component_property关键字是可选的(每个对象只有两个参数)。为了清晰起见,本示例中包含了这些内容,但为了简洁易读,本文档的其余部分将省略这些内容。- 千万别把
dash.dependencies.Input和dcc.Input搞混了。前者仅在这些回调定义中使用,后者是一个实际组件。- 请注意,我们没有在布局中为
my_output组件的children属性设置值。Dash应用程序启动时,它会自动使用输入组件的初始值调用所有回调,以填充输出组件的初始状态。在本例中,如果将div组件指定为html.Div(id='my-output',children='Hello world'),它将在应用程序启动时被覆盖,也就是说写了等于白写,还不如不写。
这有点像用Microsoft Excel编程:每当一个单元格(输入)发生变化时,依赖该单元格(输出)的所有单元格都会自动更新。因为输出会自动对输入的变化做出反应,所以这被称为“反应式编程”(Reactice Programming)。
还记得每个组件是如何完全通过一组关键字参数来描述的吗?我们在Python中设置的这些参数成为组件的属性,这些属性现在很重要。借助Dash的交互性,我们可以使用回调动态更新这些属性中的任何一个。我们通常会更新HTML组件的children属性以显示新文本(记住,children组件负责组件的内容)或dcc.Graph的figure属性以显示新数据。我们还可以更新组件的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才能下载
运行结果如下:

在本例中,dcc.Slider的value属性是这个app的输入,dcc.Grahp的figure属性是这个app的输出。无论何时dcc.Slider的value被改变,Dash都会调用update_figure这个回调函数来实时更新图像。这个函数会根据滑块的值自动过滤DataFrame中的数据,然后将处理过的数据返回给figure,画出新的图像。
这里有几点值得注意的地方:
- 我们再此代码的最开头用Pandas这个库来加载数据:
df = pd.read_csv('''')。这个dataframe(df)是一个全局变量,能够被回调函数调用。- 将数据加载到内存可能会很昂贵。通过在应用程序开始时而不是在回调函数中加载查询数据,我们可以确保此操作只在应用程序服务器启动时执行一次。当用户访问应用程序或与应用程序交互时,该数据(df)已经存在于内存中。如果可能的话,昂贵的初始化(如下载或查询数据)应该在应用程序的全局范围内完成,而不是在回调函数中完成。
- 回调不会修改原始数据,它只通过使用pandas进行过滤来创建dataframe的副本。这一点很重要:回调绝不能修改其范围之外的变量。如果回调修改了全局状态,那么一个用户的会话可能会影响下一个用户的会话,并且当应用部署在多个进程或线程上时,这些修改不会在会话之间共享。
- 我们正在使用
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)
输出如下:

在这个例子中,只要dcc.Dropdown,dcc.Slider或dcc.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()代表的是一个格子。
代码的运行结果如下:

警告:合并输出并不总是一个好主意:
- 如果输出依赖于相同输入的部分(而不是全部),那么将它们分开可以避免不必要的更新。
- 如果输出具有相同的输入,但它们使用这些输入执行非常不同的计算,保持回调分离可以允许它们并行运行。
带链接回调的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)
代码的运行结果如下:

你可以尝试一下改变输入框里的值,你会发现只有点击SUBMIT时下面的值才会改变,这就是State的效果。
在本例中,更改dcc.Input()中的文本不会触发回调,但单击按钮会触发回调。即使它们不会触发回调函数本身,但是dcc.Input当前的值仍然会传递到回调函数中。
注意,我们通过监听html的n_clicks属性来判断是否触发回调。n_clicks是一个属性,它在每次单击组件时都会递增。dash_html_components 库中的每个组件都可以使用它,但最好和按钮配合使用。
本章总结
我们已经在Dash中介绍了回调的基本原理。Dash应用程序基于一组简单但功能强大的原则构建:用户界面可通过反应式回调进行定制。组件的每个属性/属性都可以作为回调的输出进行修改,而属性的子集(例如dcc.Dropdown组件的value属性)可以由用户通过与页面交互进行编辑。
下一章我们将学习如何将回调函数应用到画图中。
