You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Plotly Dash组件回调执行时禁用后无法恢复启用的解决方法

问题描述

需要实现Plotly Dash组件在回调触发后禁用,待回调执行完成后重新启用。现有代码能成功禁用组件,但回调结束后无法恢复启用,不确定是否因使用DashProxy和MultiplexerTransform导致问题。

原代码

from dash.exceptions import PreventUpdate
from dash_extensions.enrich import (
    Output,
    DashProxy,
    html,
    Input,
    MultiplexerTransform,
)
from time import sleep

app = DashProxy(__name__, transforms=[MultiplexerTransform()])

app.layout = html.Div(
    [
        html.Button(id="button", children="input-button", style={"color": "#FF0000"}),
        html.Div(id="trigger", children=None, style={"display": "none"})
    ],
)

@app.callback(
    [
        Output("button", "disabled"),
        Output("button", "style"),
        Output("trigger", "children"),
    ],
    [
        Input("trigger", "children")
    ],
    prevent_initial_call=True
)
def enable_components_after_export(trigger):
    if trigger == 1:
        return [
            False, {"color": "#FF0000"}, 0
        ]

    raise PreventUpdate


@app.callback(
    [
        Output("button", "disabled"),
        Output("button", "style")
    ],
    [
        Input("button", "n_clicks"),
    ],
    prevent_initial_call=True
)
def disable_components_on_export_button_click(button):
    if (button is not None and button > 0):
        return [
            True, {"color": "#808080"}
        ]

    raise PreventUpdate

@app.callback(
    [
        Output("trigger", "children")
    ],
    [
        Input("button", "n_clicks"),
    ],
    prevent_initial_call=True
)
def callback_function(button):
    if button is not None and button > 0:
        sleep(5)
        return [1]
    
    raise PreventUpdate

if __name__ == "__main__":
    app.run(debug=False)

包版本

dash                      2.12.1                   pypi_0    pypi       
dash-bootstrap-components 1.5.0                    pypi_0    pypi       
dash-core-components      2.0.0                    pypi_0    pypi       
dash-extensions           0.1.11                   pypi_0    pypi       
dash-html-components      2.0.0                    pypi_0    pypi       
dash-renderer             1.9.1                    pypi_0    pypi 

问题分析

原代码的核心问题在于:

  • 多个回调同时监听button.n_clicks,且存在对同一组件属性(button.disabled、button.style)的重复输出,导致回调执行顺序不可控,最终覆盖了启用组件的操作。
  • 同步sleep(5)会阻塞Dash主线程,导致整个应用在回调执行期间无响应,后续的启用回调无法及时触发。
  • MultiplexerTransform允许同一输入被多个回调监听,但并未解决回调输出冲突的问题。

正确实现方式

方案1:使用单个回调 + 后台任务(推荐)

利用Dash 2.7+支持的后台回调,避免主线程阻塞,同时在同一个回调中完成禁用、执行任务、启用的完整流程:

from dash import Dash, html, Input, Output, State, callback
from dash.exceptions import PreventUpdate
import time
from dash.long_callback import DiskcacheLongCallbackManager
import diskcache

cache = diskcache.Cache("./cache")
long_callback_manager = DiskcacheLongCallbackManager(cache)

app = Dash(__name__, long_callback_manager=long_callback_manager)

app.layout = html.Div(
    [
        html.Button(id="button", children="input-button", style={"color": "#FF0000"}),
    ]
)

@callback(
    [Output("button", "disabled"), Output("button", "style")],
    Input("button", "n_clicks"),
    State("button", "disabled"),
    State("button", "style"),
    prevent_initial_call=True,
    running=[
        (Output("button", "disabled"), True, False),
        (Output("button", "style"), {"color": "#808080"}, {"color": "#FF0000"})
    ]
)
def handle_button_click(n_clicks, current_disabled, current_style):
    if not n_clicks:
        raise PreventUpdate
    
    # 模拟耗时任务
    time.sleep(5)
    
    # 任务完成后返回启用状态
    return False, {"color": "#FF0000"}

if __name__ == "__main__":
    app.run(debug=False)

方案2:基于DashProxy和MultiplexerTransform的修正

如果必须使用DashProxy和MultiplexerTransform,可以通过dcc.Store统一管理状态,避免回调输出冲突:

from dash.exceptions import PreventUpdate
from dash_extensions.enrich import (
    Output,
    DashProxy,
    html,
    Input,
    State,
    MultiplexerTransform,
    dcc
)
from time import sleep

app = DashProxy(__name__, transforms=[MultiplexerTransform()])

app.layout = html.Div(
    [
        html.Button(id="button", children="input-button", style={"color": "#FF0000"}),
        dcc.Store(id="task_status", data="idle")  # 用Store跟踪任务状态:idle/running/finished
    ],
)

# 监听按钮点击,触发任务并更新状态为finished
@app.callback(
    Output("task_status", "data"),
    Input("button", "n_clicks"),
    State("task_status", "data"),
    prevent_initial_call=True
)
def start_task(n_clicks, status):
    if n_clicks and status == "idle":
        sleep(5)  # 模拟耗时任务
        return "finished"
    raise PreventUpdate

# 根据任务状态更新按钮状态
@app.callback(
    [Output("button", "disabled"), Output("button", "style")],
    Input("task_status", "data"),
    prevent_initial_call=True
)
def update_button(status):
    if status == "finished":
        return False, {"color": "#FF0000"}
    # 任务执行中(回调未完成时,状态仍为idle,此时禁用按钮)
    if status == "idle" and len([c for c in app.callback_list if c.inputs[0].id == "button" and c.running]):
        return True, {"color": "#808080"}
    return False, {"color": "#FF0000"}

if __name__ == "__main__":
    app.run(debug=False)

关键说明

  • 后台回调(方案1)是最优解,它将耗时任务移至后台线程/进程,避免阻塞主线程,同时通过running参数自动管理组件的禁用/启用状态,无需手动处理。
  • 若坚持使用MultiplexerTransform,需通过dcc.Store统一管理任务状态,确保只有一个回调负责更新组件属性,避免输出冲突。

内容的提问来源于stack exchange,提问作者disguisedtoast

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 03:51:00