如何在单独的文件中定义回调? (阴谋破折号)

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)

我认为这样做更明确。

晚会有点晚了,但我发现这是一种简单的方法:

  1. 您创建一个名为 'callbacks.py' 的单独脚本(例如)
  2. 在 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
  1. 在主脚本中,只需导入函数并在实例化 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