如何在单独的文件中定义回调? (阴谋破折号)
How to define callbacks in separate files? (plotly dash)
背景
Dash web 应用程序有一个 dash 应用程序实例,通常命名为 app
,并像这样启动:
app = dash.Dash(__name__)
然后,使用 callback
装饰器将回调添加到应用程序:
@app.callback(...)
def my_function(...):
# do stuff.
在您找到的大多数教程中,回调都是在 app.py
中使用所有应用程序布局定义的。这当然只是 MWE 的做事方式。在实际应用程序中,将代码分离到模块和包中会大大提高可读性和可维护性,但天真地将回调和布局分离只会导致循环导入。
问题
在单页应用中将回调和布局与 app.py
分开的正确方法是什么?
MWE
这是一个有问题的最小(非)工作示例
文件结构
.
├── my_dash_app
│ ├── app.py
│ └── views
│ ├── first_view.py
│ └── __init__.py
└── setup.py
setup.py
import setuptools
setuptools.setup(
name='dash-minimal-realworld',
version='1.0.0',
install_requires=['dash>=1.12.0'],
packages=setuptools.find_packages(),
)
app.py
import dash
from my_dash_app.views.first_view import make_layout
app = dash.Dash(__name__)
app.layout = make_layout()
if __name__ == '__main__':
app.run_server(debug=True)
first_view.py
from dash.dependencies import Input, Output
import dash_core_components as dcc
import dash_html_components as html
from my_dash_app.app import app
def make_layout():
return html.Div([
dcc.Input(id='my-id', value='initial value', type='text'),
html.Div(id='my-div')
])
@app.callback(Output(component_id='my-div', component_property='children'),
[Input(component_id='my-id', component_property='value')])
def update_output_div(input_value):
return 'You\'ve entered "{}"'.format(input_value)
运行 python ./my_dash_app/app.py
导致循环依赖:
ImportError: cannot import name 'make_layout' from 'my_dash_app.views.first_view' (c:\tmp\dash_minimal_realworld\my_dash_app\views\first_view.py)
我不认为 (但我可能是错的) 本身有一个正确的方法,但是你可以用一个中央模块(maindash.py
) 围绕您的启动代码 app = dash.Dash(__name__)
,并具有不同的 回调 只需从 my_dash_app.maindash
导入 app
。这将在它们自己的单独模块中设置回调,但将那个中央模块重新用于 app
实例。
像这样显示它的概览是最简单的:
app.py
是启动一切的主脚本。 maindash.py
负责创建主应用程序实例。 first_view.py
是定义装饰器以设置所有回调的地方。
结果如下:
.
├── my_dash_app
│ ├── app.py
│ ├── maindash.py
│ └── views
│ ├── first_view.py
│ └── __init__.py
└── setup.py
由于在 Python 中重复使用了导入,因此从不同的其他模块(例如事件处理程序和主脚本)多次执行 from my_dash_app.maindash import app
并没有真正的危害。他们将共享相同的导入实例 - 因此也重新使用 dash.Dash()
实例。
只需确保在设置处理程序之前导入中央模块,就可以开始了。
以下是为测试而分离的代码片段:
app.py
from my_dash_app.maindash import app
from my_dash_app.views.first_view import make_layout
if __name__ == '__main__':
app.layout = make_layout()
app.run_server(debug=True)
maindash.py
import dash
app = dash.Dash(__name__)
first_view.py
from my_dash_app.maindash import app
from dash.dependencies import Input, Output
import dash_core_components as dcc
import dash_html_components as html
def make_layout():
return html.Div([
dcc.Input(id='my-id', value='initial value', type='text'),
html.Div(id='my-div')
])
@app.callback(Output(component_id='my-div', component_property='children'),
[Input(component_id='my-id', component_property='value')])
def update_output_div(input_value):
return 'You\'ve entered "{}"'.format(input_value)
我知道在这里回答你的问题为时已晚,但也许其他人会发现它有用。
我希望能够在单独的文件中创建回调,但我认为虽然从主仪表板模块导入应用程序效果很好,但阅读代码的其他人可能不清楚。
我创建了一个用于初始化回调的回调管理器。此管理器附加到主应用模块中的一个应用。
callbacks_manager.py
from dataclasses import dataclass, field
from typing import Callable, List, Union
from dash.dependencies import handle_callback_args
from dash.dependencies import Input, Output, State
@dataclass
class Callback:
func: Callable
outputs: Union[Output, List[Output]]
inputs: Union[Input, List[Input]]
states: Union[State, List[State]] = field(default_factory=list)
kwargs: dict = field(default_factory=lambda: {"prevent_initial_call": False})
class CallbackManager:
def __init__(self):
self._callbacks = []
def callback(self, *args, **kwargs):
output, inputs, state, prevent_initial_call = handle_callback_args(
args, kwargs
)
def wrapper(func):
self._callbacks.append(Callback(func,
output,
inputs,
state,
{"prevent_initial_callback": prevent_initial_call}))
return wrapper
def attach_to_app(self, app):
for callback in self._callbacks:
app.callback(
callback.outputs, callback.inputs, callback.states, **callback.kwargs
)(callback.func)
callbacks.py
import dash
from callback_manager import CallbackManager
callback_manager = CallbackManager()
@callback_manager.callback(
dash.dependencies.Output('label', 'children'),
[dash.dependencies.Input('call_btn', 'n_clicks')])
def update_label(n_clicks):
if n_clicks > 0:
return "Callback called!"
app.py
import dash
import dash_html_components as html
from callbacks import callback_manager
app = dash.Dash(__name__)
callback_manager.attach_to_app(app)
app.layout = html.Div([
html.Div(id="label"),
html.Button('Call callback', id='call_btn', n_clicks=0),
])
if __name__ == '__main__':
app.run_server(debug=True)
请注意,您可以拥有多个带有回调的文件,并使用 as
关键字导入它们:
from callbacks1 import callback_manager as callback_manager1
from callbacks2 import callback_manager as callback_manager2
app = dash.Dash(__name__)
callback_manager1.attach_to_app(app)
callback_manager2.attach_to_app(app)
我认为这样做更明确。
晚会有点晚了,但我发现这是一种简单的方法:
- 您创建一个名为 'callbacks.py' 的单独脚本(例如)
- 在 callbacks.py 中定义一个函数,它将 dash.Dash 对象(即应用程序)作为参数(当然,如果需要,您可以传递更多参数),并在其中定义所有回调正如您通常在主脚本中所做的那样:
def get_callbacks(app):
@app.callback([Output("figure1", "figure")],
[Input("child1", "value")])
def callback1(figure):
return
@app.callback([Output("figure2", "figure")],
[Input("child2", "value")])
def callback2(figure):
return
- 在主脚本中,只需导入函数并在实例化 dash.Dash 对象后调用它,将相同的对象传递给它:
import dash
from callbacks import get_callbacks
import layout
app = dash.Dash(__name__)
app.layout = layout.layout
get_callbacks(app)
您可以只使用装饰器 @dash.callback
而不是 @app.callback
。然后删除 first_view.py
中的 from my_dash_app.app import app
行,您将摆脱循环依赖。
来自文档:@dash.callback
是 Dash 2.0 中引入的 @app.callback (where app = dash.Dash())
的替代方法。它允许您在不定义或导入应用程序对象的情况下注册回调。调用签名是相同的,在所有情况下都可以使用它代替 app.callback
。
背景
Dash web 应用程序有一个 dash 应用程序实例,通常命名为 app
,并像这样启动:
app = dash.Dash(__name__)
然后,使用 callback
装饰器将回调添加到应用程序:
@app.callback(...)
def my_function(...):
# do stuff.
在您找到的大多数教程中,回调都是在 app.py
中使用所有应用程序布局定义的。这当然只是 MWE 的做事方式。在实际应用程序中,将代码分离到模块和包中会大大提高可读性和可维护性,但天真地将回调和布局分离只会导致循环导入。
问题
在单页应用中将回调和布局与 app.py
分开的正确方法是什么?
MWE
这是一个有问题的最小(非)工作示例
文件结构
.
├── my_dash_app
│ ├── app.py
│ └── views
│ ├── first_view.py
│ └── __init__.py
└── setup.py
setup.py
import setuptools
setuptools.setup(
name='dash-minimal-realworld',
version='1.0.0',
install_requires=['dash>=1.12.0'],
packages=setuptools.find_packages(),
)
app.py
import dash
from my_dash_app.views.first_view import make_layout
app = dash.Dash(__name__)
app.layout = make_layout()
if __name__ == '__main__':
app.run_server(debug=True)
first_view.py
from dash.dependencies import Input, Output
import dash_core_components as dcc
import dash_html_components as html
from my_dash_app.app import app
def make_layout():
return html.Div([
dcc.Input(id='my-id', value='initial value', type='text'),
html.Div(id='my-div')
])
@app.callback(Output(component_id='my-div', component_property='children'),
[Input(component_id='my-id', component_property='value')])
def update_output_div(input_value):
return 'You\'ve entered "{}"'.format(input_value)
运行 python ./my_dash_app/app.py
导致循环依赖:
ImportError: cannot import name 'make_layout' from 'my_dash_app.views.first_view' (c:\tmp\dash_minimal_realworld\my_dash_app\views\first_view.py)
我不认为 (但我可能是错的) 本身有一个正确的方法,但是你可以用一个中央模块(maindash.py
) 围绕您的启动代码 app = dash.Dash(__name__)
,并具有不同的 回调 只需从 my_dash_app.maindash
导入 app
。这将在它们自己的单独模块中设置回调,但将那个中央模块重新用于 app
实例。
像这样显示它的概览是最简单的:
app.py
是启动一切的主脚本。 maindash.py
负责创建主应用程序实例。 first_view.py
是定义装饰器以设置所有回调的地方。
结果如下:
.
├── my_dash_app
│ ├── app.py
│ ├── maindash.py
│ └── views
│ ├── first_view.py
│ └── __init__.py
└── setup.py
由于在 Python 中重复使用了导入,因此从不同的其他模块(例如事件处理程序和主脚本)多次执行 from my_dash_app.maindash import app
并没有真正的危害。他们将共享相同的导入实例 - 因此也重新使用 dash.Dash()
实例。
只需确保在设置处理程序之前导入中央模块,就可以开始了。
以下是为测试而分离的代码片段:
app.py
from my_dash_app.maindash import app
from my_dash_app.views.first_view import make_layout
if __name__ == '__main__':
app.layout = make_layout()
app.run_server(debug=True)
maindash.py
import dash
app = dash.Dash(__name__)
first_view.py
from my_dash_app.maindash import app
from dash.dependencies import Input, Output
import dash_core_components as dcc
import dash_html_components as html
def make_layout():
return html.Div([
dcc.Input(id='my-id', value='initial value', type='text'),
html.Div(id='my-div')
])
@app.callback(Output(component_id='my-div', component_property='children'),
[Input(component_id='my-id', component_property='value')])
def update_output_div(input_value):
return 'You\'ve entered "{}"'.format(input_value)
我知道在这里回答你的问题为时已晚,但也许其他人会发现它有用。
我希望能够在单独的文件中创建回调,但我认为虽然从主仪表板模块导入应用程序效果很好,但阅读代码的其他人可能不清楚。
我创建了一个用于初始化回调的回调管理器。此管理器附加到主应用模块中的一个应用。
callbacks_manager.py
from dataclasses import dataclass, field
from typing import Callable, List, Union
from dash.dependencies import handle_callback_args
from dash.dependencies import Input, Output, State
@dataclass
class Callback:
func: Callable
outputs: Union[Output, List[Output]]
inputs: Union[Input, List[Input]]
states: Union[State, List[State]] = field(default_factory=list)
kwargs: dict = field(default_factory=lambda: {"prevent_initial_call": False})
class CallbackManager:
def __init__(self):
self._callbacks = []
def callback(self, *args, **kwargs):
output, inputs, state, prevent_initial_call = handle_callback_args(
args, kwargs
)
def wrapper(func):
self._callbacks.append(Callback(func,
output,
inputs,
state,
{"prevent_initial_callback": prevent_initial_call}))
return wrapper
def attach_to_app(self, app):
for callback in self._callbacks:
app.callback(
callback.outputs, callback.inputs, callback.states, **callback.kwargs
)(callback.func)
callbacks.py
import dash
from callback_manager import CallbackManager
callback_manager = CallbackManager()
@callback_manager.callback(
dash.dependencies.Output('label', 'children'),
[dash.dependencies.Input('call_btn', 'n_clicks')])
def update_label(n_clicks):
if n_clicks > 0:
return "Callback called!"
app.py
import dash
import dash_html_components as html
from callbacks import callback_manager
app = dash.Dash(__name__)
callback_manager.attach_to_app(app)
app.layout = html.Div([
html.Div(id="label"),
html.Button('Call callback', id='call_btn', n_clicks=0),
])
if __name__ == '__main__':
app.run_server(debug=True)
请注意,您可以拥有多个带有回调的文件,并使用 as
关键字导入它们:
from callbacks1 import callback_manager as callback_manager1
from callbacks2 import callback_manager as callback_manager2
app = dash.Dash(__name__)
callback_manager1.attach_to_app(app)
callback_manager2.attach_to_app(app)
我认为这样做更明确。
晚会有点晚了,但我发现这是一种简单的方法:
- 您创建一个名为 'callbacks.py' 的单独脚本(例如)
- 在 callbacks.py 中定义一个函数,它将 dash.Dash 对象(即应用程序)作为参数(当然,如果需要,您可以传递更多参数),并在其中定义所有回调正如您通常在主脚本中所做的那样:
def get_callbacks(app):
@app.callback([Output("figure1", "figure")],
[Input("child1", "value")])
def callback1(figure):
return
@app.callback([Output("figure2", "figure")],
[Input("child2", "value")])
def callback2(figure):
return
- 在主脚本中,只需导入函数并在实例化 dash.Dash 对象后调用它,将相同的对象传递给它:
import dash
from callbacks import get_callbacks
import layout
app = dash.Dash(__name__)
app.layout = layout.layout
get_callbacks(app)
您可以只使用装饰器 @dash.callback
而不是 @app.callback
。然后删除 first_view.py
中的 from my_dash_app.app import app
行,您将摆脱循环依赖。
来自文档:@dash.callback
是 Dash 2.0 中引入的 @app.callback (where app = dash.Dash())
的替代方法。它允许您在不定义或导入应用程序对象的情况下注册回调。调用签名是相同的,在所有情况下都可以使用它代替 app.callback
。