本章描述如何使用回调函数来制作你的Dash应用程序:当输入组件的属性发生变化时,Dash会自动调用这些Python函数。

1.简单栗子

  1. import dash
  2. import dash_core_components as dcc
  3. import dash_html_components as html
  4. from dash.dependencies import Input, Output
  5. external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
  6. app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
  7. app.layout = html.Div([
  8. html.H6("Change the value in the text box to see callbacks in action!"),
  9. html.Div(["Input: ",
  10. dcc.Input(id='my-input', value='initial value', type='text')]),
  11. html.Br(),
  12. html.Div(id='my-output'),
  13. ])
  14. @app.callback(
  15. Output(component_id='my-output', component_property='children'),
  16. Input(component_id='my-input', component_property='value')
  17. )
  18. def update_output_div(input_value):
  19. return 'Output: {}'.format(input_value)
  20. if __name__ == '__main__':
  21. app.run_server(debug=True)

image.png

让我们来分析一下这个例子:

  1. app.callback装饰器通过声明,描述应用程序界面的“输入”与“输出”项;
  2. 我们的应用程序的**Input****output**只是特定组件的属性。在本例中,我们**Input****value**值是**ID**为 “**my-input**“。我们的**Output****ID**为“**my-output**”的组件的“**children**”属性。
  3. 每当输入属性发生变化时,回调装饰器包装的函数将被自动调用。Dash将输入属性的新值作为输入参数提供给函数,Dash使用函数返回的内容更新输出组件的属性。
  4. 不要混淆dash.dependencies.Inputdash_core_components对象。前者只是在这些回调中使用,而后者是一个实际的组件。
  5. The component_id and component_property可以被省略
  6. Dash应用程序启动时,会自动使用输入组件的初始值,调用所有的回调函数,以填充输出组件的初始值。所以,不要在layout中设置 my-div组件的children特性,本例中,如果指定了 html.Div(id='my-div', children='Hello world') 的内容,应用启动时会被覆盖。这种方式类似于Microsoft Excel编程:当单元格的内容发生变化时,依赖于该单元格的所有单元格的内容,都将自动更新。这称为 “反应式编程” (Reactive Programming)

@callback的详细用法:

  1. 通过编写这个装饰器,我们告诉Dash在“input”组件(文本框)的值发生变化时为我们调用这个函数,以便更新页面上“output”组件的子组件(HTML div)
  2. 可以使用任何的函数名称跟在@callback之后,但必须在回调函数中使用与定义中相同的名称(就和python语法一样)。约定是用名称来描述回调输出。
  3. 参数是有位置的。第一个(最底下那个)是Input,后面跟任何state,顺序与装饰器中相同。
  4. 当把Dash组件作为@appback的输入或输出时,你必须使用与app.layout中Dash组件相同的id。回调装饰。
  5. callback后面紧跟着函数,不能有空行。
  6. If you’re curious about what the decorator syntax means under the hood, you can read this StackOverflow answer and learn more about decorators by reading PEP 318 — Decorators for Functions and Methods.

2.滑块

  1. import dash
  2. import dash_core_components as dcc
  3. import dash_html_components as html
  4. from dash.dependencies import Input, Output
  5. import plotly.express as px
  6. import pandas as pd
  7. df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/gapminderDataFiveYear.csv')
  8. external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
  9. app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
  10. app.layout = html.Div([
  11. dcc.Graph(id='graph-with-slider'),
  12. dcc.Slider(
  13. id='year-slider',
  14. min=df['year'].min(),
  15. max=df['year'].max(),
  16. value=df['year'].min(),
  17. marks={str(year): str(year) for year in df['year'].unique()},
  18. step=None
  19. )
  20. ])
  21. @app.callback(
  22. Output('graph-with-slider', 'figure'),
  23. Input('year-slider', 'value'))
  24. def update_figure(selected_year):
  25. filtered_df = df[df.year == selected_year]
  26. fig = px.scatter(filtered_df, x="gdpPercap", y="lifeExp",
  27. size="pop", color="continent", hover_name="country",
  28. log_x=True, size_max=55)
  29. fig.update_layout(transition_duration=500)
  30. return fig
  31. if __name__ == '__main__':
  32. app.run_server(debug=True)

image.png

  1. 本例中,app的输入是 **Slider** 的属性value,app的输出是 **Graph** 的属性figure。当 Slidervalue 变化时,Dash用新值调用回调函数 update_figure,该函数使用此新值过滤数据框,构造 figure 对象,并将其返回到Dash应用程序中,作为输出;
  2. 使用关键字参数进行组件描述,很重要。通过Dash交互性,使用回调函数,可以动态地更新这些特性。如:更新组件的 **children** 属性从而更新文本内容、更新 **dcc.Graph** 组件的**figure** 属性从而更新数据、更新组件的 **style** 属性从而更新画布样式、更新 **dcc.Dropdown** 组件的 **options** 从而更新下拉菜单;
  3. 将数据加载至内存并进行计算的代价很高,所以尽量在应用的全局范围内下载或查询数据,避免在回调函数里进行这类操作,确保用户访问或与应用交互时,数据(df)已经载入至内存。本例中df获取的数据是全局的,可以被回调函数读取;
  4. 回调函数不会修改原始数据,只是通过**Pandas**的过滤器来筛选数据,并创建DataFrame的副本。这点非常重要:不要在回调函数范围之外更改变量。如果在全局状态下调整回调函数,某一用户的会话就可能影响下一用户的会话,特别是应用部署在多进程或多线程的环境时,这些修改可能会导致跨会话数据分享出现问题;

3.多端输入

在Dash中,任何“Input”都可以有多个“Output”组件。下面是一个简单的例子,它将五个输入(两个Dropdown组件的value属性、两个RadioItems组件和一个Slider组件)绑定到一个输出组件(Graph组件的figure属性)。回调函数的第二个参数,列表中列举了所有的五个输入项dash.dependencies.Input

  1. import dash
  2. import dash_core_components as dcc
  3. import dash_html_components as html
  4. from dash.dependencies import Input, Output
  5. import plotly.express as px
  6. import pandas as pd
  7. external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
  8. app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
  9. df = pd.read_csv('https://plotly.github.io/datasets/country_indicators.csv')
  10. available_indicators = df['Indicator Name'].unique()
  11. app.layout = html.Div([
  12. html.Div([
  13. html.Div([
  14. dcc.Dropdown(
  15. id='xaxis-column',
  16. options=[{'label': i, 'value': i} for i in available_indicators],
  17. value='Fertility rate, total (births per woman)'
  18. ),
  19. dcc.RadioItems(
  20. id='xaxis-type',
  21. options=[{'label': i, 'value': i} for i in ['Linear', 'Log']],
  22. value='Linear',
  23. labelStyle={'display': 'inline-block'}
  24. )
  25. ],
  26. style={'width': '48%', 'display': 'inline-block'}),
  27. html.Div([
  28. dcc.Dropdown(
  29. id='yaxis-column',
  30. options=[{'label': i, 'value': i} for i in available_indicators],
  31. value='Life expectancy at birth, total (years)'
  32. ),
  33. dcc.RadioItems(
  34. id='yaxis-type',
  35. options=[{'label': i, 'value': i} for i in ['Linear', 'Log']],
  36. value='Linear',
  37. labelStyle={'display': 'inline-block'}
  38. )
  39. ],style={'width': '48%', 'float': 'right', 'display': 'inline-block'})
  40. ]),
  41. dcc.Graph(id='indicator-graphic'),
  42. dcc.Slider(
  43. id='year--slider',
  44. min=df['Year'].min(),
  45. max=df['Year'].max(),
  46. value=df['Year'].max(),
  47. marks={str(year): str(year) for year in df['Year'].unique()},
  48. step=None
  49. )
  50. ])
  51. @app.callback(
  52. Output('indicator-graphic', 'figure'),
  53. Input('xaxis-column', 'value'),
  54. Input('yaxis-column', 'value'),
  55. Input('xaxis-type', 'value'),
  56. Input('yaxis-type', 'value'),
  57. Input('year--slider', 'value'))
  58. def update_graph(xaxis_column_name, yaxis_column_name,
  59. xaxis_type, yaxis_type,
  60. year_value):
  61. dff = df[df['Year'] == year_value]
  62. fig = px.scatter(x=dff[dff['Indicator Name'] == xaxis_column_name]['Value'],
  63. y=dff[dff['Indicator Name'] == yaxis_column_name]['Value'],
  64. hover_name=dff[dff['Indicator Name'] == yaxis_column_name]['Country Name'])
  65. fig.update_layout(margin={'l': 40, 'b': 40, 't': 10, 'r': 0}, hovermode='closest')
  66. fig.update_xaxes(title=xaxis_column_name,
  67. type='linear' if xaxis_type == 'Linear' else 'log')
  68. fig.update_yaxes(title=yaxis_column_name,
  69. type='linear' if yaxis_type == 'Linear' else 'log')
  70. return fig
  71. if __name__ == '__main__':
  72. app.run_server(debug=True)

image.png

  1. 在本例中,每当DropdownSliderRadioItems组件的value属性更改时,就会调用update_graph函数。
  2. update_graph函数的输入参数是每个输入属性的新值或当前值,按指定的顺序排列。
  3. 即使一次只更改一个输入(用户在给定时刻只能更改单个下拉列表的值),Dash也会收集所有指定输入属性的当前状态,并将它们传递到函数中。你的回调函数总是保证会被传递给应用程序的代表状态。


4.多端输出

  1. import dash
  2. import dash_core_components as dcc
  3. import dash_html_components as html
  4. from dash.dependencies import Input, Output
  5. external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
  6. app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
  7. app.layout = html.Div([
  8. dcc.Input(
  9. id='num-multi',
  10. type='number',
  11. value=5
  12. ),
  13. html.Table([
  14. html.Tr([html.Td(['x', html.Sup(2)]), html.Td(id='square')]),
  15. html.Tr([html.Td(['x', html.Sup(3)]), html.Td(id='cube')]),
  16. html.Tr([html.Td([2, html.Sup('x')]), html.Td(id='twos')]),
  17. html.Tr([html.Td([3, html.Sup('x')]), html.Td(id='threes')]),
  18. html.Tr([html.Td(['x', html.Sup('x')]), html.Td(id='x^x')]),
  19. ]),
  20. ])
  21. @app.callback(
  22. Output('square', 'children'),
  23. Output('cube', 'children'),
  24. Output('twos', 'children'),
  25. Output('threes', 'children'),
  26. Output('x^x', 'children'),
  27. Input('num-multi', 'value'))
  28. def callback_a(x):
  29. return x**2, x**3, 2**x, 3**x, x**x
  30. if __name__ == '__main__':
  31. app.run_server(debug=True)

image.png

  • 一个Dash回调函数只能更新一个输出属性。要想实现多重输出,需要编写多个函数;
  • 具体方法:将需要更新的所有属性,作为列表添加到装饰器中,并从回调中返回多个输出项。如果两个输出依赖于相同的计算密集型中间结果,例如慢速数据库查询,推荐使用该方法;
  • 组合输出并不总是一个好主意:1)如果输出依赖于某些但不是所有相同的输入,则将它们分开可以避免不必要的更新;2)如果它们具有相同的输入,但使用这些输入进行独立计算,则将回调分开,可以实现并行运行它们;

5.Chained Callbacks

一个回调函数的输出是另一个回调函数的输入。

  1. # -*- coding: utf-8 -*-
  2. import dash
  3. import dash_core_components as dcc
  4. import dash_html_components as html
  5. from dash.dependencies import Input, Output
  6. external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
  7. app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
  8. all_options = {
  9. 'America': ['New York City', 'San Francisco', 'Cincinnati'],
  10. 'Canada': [u'Montréal', 'Toronto', 'Ottawa']
  11. }
  12. app.layout = html.Div([
  13. dcc.RadioItems(
  14. id='countries-radio',
  15. options=[{'label': k, 'value': k} for k in all_options.keys()],
  16. value='America'
  17. ),
  18. html.Hr(),
  19. dcc.RadioItems(id='cities-radio'),
  20. html.Hr(),
  21. html.Div(id='display-selected-values')
  22. ])
  23. @app.callback(
  24. Output('cities-radio', 'options'),
  25. Input('countries-radio', 'value'))
  26. def set_cities_options(selected_country):
  27. return [{'label': i, 'value': i} for i in all_options[selected_country]]
  28. @app.callback(
  29. Output('cities-radio', 'value'),
  30. Input('cities-radio', 'options'))
  31. def set_cities_value(available_options):
  32. return available_options[0]['value']
  33. @app.callback(
  34. Output('display-selected-values', 'children'),
  35. Input('countries-radio', 'value'),
  36. Input('cities-radio', 'value'))
  37. def set_display_children(selected_country, selected_city):
  38. return u'{} is a city in {}'.format(
  39. selected_city, selected_country,
  40. )
  41. if __name__ == '__main__':
  42. app.run_server(debug=True)

image.png

  • 链式回调:将输出和输入链接在一起,即一个回调函数的输出是另一个回调函数的输入;
  • 此模式用于创建动态UI,其中一个输入组件更新下一个输入组件的可用选项;
  • 第二个单选按钮RadioItems的选项,基于第一个回调函数传递的单选按钮RadioItems中选择的值;
  • 第二个回调函数设置了options特性改变时的初始值:将自身设置为options数组中的第一个值;
  • 最后的回调函数,显示了每个组件中的可选内容。如果更改了城市单选按钮RadioItems组件的value属性,则Dash将等待,直到value更新状态组件后,再调用最后的回调函数。

6.状态回调

  1. # -*- coding: utf-8 -*-
  2. import dash
  3. import dash_core_components as dcc
  4. import dash_html_components as html
  5. from dash.dependencies import Input, Output, State
  6. external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
  7. app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
  8. app.layout = html.Div([
  9. dcc.Input(id='input-1-state', type='text', value='Montréal'),
  10. dcc.Input(id='input-2-state', type='text', value='Canada'),
  11. html.Button(id='submit-button-state', n_clicks=0, children='Submit'),
  12. html.Div(id='output-state')
  13. ])
  14. @app.callback(Output('output-state', 'children'),
  15. Input('submit-button-state', 'n_clicks'),
  16. State('input-1-state', 'value'),
  17. State('input-2-state', 'value'))
  18. def update_output(n_clicks, input1, input2):
  19. return u'''
  20. The Button has been pressed {} times,
  21. Input 1 is "{}",
  22. and Input 2 is "{}"
  23. '''.format(n_clicks, input1, input2)
  24. if __name__ == '__main__':
  25. app.run_server(debug=True)

image.png

在这个例子中,更改输入框不会有返回值的改变,只有点击SUBMIT才会更新返回值。
dcc.Input的值依然会传入到回调参数中,但是仅仅是作为State传入,当SUBMIT的控件触发后,才会把State的value传入到函数中。

7.小结

  1. Dash应用是基于下述简单但强大的原则进行构建的:通过响应式与函数式的Python回调函数,自定义声明式的UI;
  2. 声明式组件中的每个元素属性,都可以通过回调函数和属性子集进行更新,比如dcc.Dropdown的value特性,这样用户就可以在交互界面中进行编辑。

注释参考:https://www.jianshu.com/p/cb0dc98e00bc
官方文档:https://dash.plotly.com/basic-callbacks