简体   繁体   中英

How to avoid firing undesirable callbacks in Python Dash?

I wrote a Dash app whose source code is shared below:

import dash
from dash import Dash
import dash_html_components as html
import dash_core_components as dcc
import dash_bootstrap_components as dbc
from dash.dependencies import Input, Output
import time


app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP], url_base_pathname='/self_serve/')
server = app.server

reset_flag = False
counter = 0

app.title = 'jammed dash app'
app.layout = html.Div([
    # buttons
    dcc.Input(id='main',placeholder='Main Value'),
    dcc.Input(id='filter1',placeholder='filter 1'),
    dcc.Input(id='filter2',placeholder='filter 2'),
    dcc.Input(id='filter3',placeholder='filter 3'),
    dcc.Input(id='filter4',placeholder='filter 4'),
    html.Div(id='output',children='')
])

#metric list
@app.callback(
    Output(component_id='filter1',component_property='value'),
    Output(component_id='filter2',component_property='value'),
    Output(component_id='filter3',component_property='value'),
    Output(component_id='filter4',component_property='value'),
    [
    Input(component_id='main',component_property='value')
    ]
)
def update_filter(main):
    # clear up all filters if main is provided
    global reset_flag
    reset_flag = True
    return '','','',''


@app.callback(
    Output(component_id='output',component_property='children'),
    [
        Input(component_id='main',component_property='value'),
        Input(component_id='filter1',component_property='value'),
        Input(component_id='filter2',component_property='value'),
        Input(component_id='filter3',component_property='value'),
        Input(component_id='filter4',component_property='value'),
    ]
)
def calculate(*args):
    # do some intensive calculation based on the inputs, but I do not want the clearing action to trigger this callback undesirably

    ctx = dash.callback_context
    print('\n')
    print('************ inside calculate *************')
    print('triggered:', ctx.triggered)
    print('inputs:', ctx.inputs)

    # my idea for solving this problem
    global reset_flag, counter
    if reset_flag:
        counter += 1
        if counter <= 4:
            print('counter:',counter)
            print('reset_flag:',reset_flag)
            return ''
        else:
            reset_flag = False
            counter = 0
            print('we passed into the correct flow!')
            pass

    # below is some intensive calculation using pandas.read_sql(), substituted by time.sleep()
    print('Wait 10 seconds here')
    time.sleep(10)
    output = ''
    for item in args:
        if item:
            output += item
    print('output:',output)
    return output


if __name__ == '__main__':
    app.run_server(debug=True)

I need to perform some intensive calculations (eg sql code) in the 2nd callback ("calculate"). I would also like to clear up all filter elements whenever value in "main" changes, which is implemented in the 1st callback ("update_filter"). The issue is that, when each filter is cleared in the 1st callback, dash is making call to the 2nd callback in quick succession and the program is jammed.

My question is: how to avoid firing the 2nd callback when the 1st callback is called? My idea is to track the number of times the 2nd callback is called "redundantly" and letting the "correct" call to pass while returning empty output for "redundant" calls. I tried to implement using a global counter, but this is to no avail.

And in general, what is the best/common practice for avoiding these undesirable, chained callback firing?

Thanks a lot in advance!

In your case, what you might want to do is:

Input(main) -> Output(filter)  # reset filter
# when filter changes, take main's state and use it to update output
Input(filter), State(main) -> Output(output)  

This doesn't block because it is a chained callback: changing main clears filter , and changing filter triggers the second callback which takes main 's state and updates output .

For time-intensive operations, you might want to cache those results. There is an entry in the doc showing this: https://dash.plotly.com/performance

Here is my rather crude implementation of the lock on your calculate function using a file. Global variables, I think, are problematic given various threading issues etc. The idea is when calculate does its heavy calculations it puts a '1' into a file, and when it is done it puts '0' there. If in the meantime the function is called again, it checks the lockfile and if there is '1' there it exists without triggering recalculation.

import dash
from dash import Dash
import dash_html_components as html
import dash_core_components as dcc
import dash_bootstrap_components as dbc
from dash.dependencies import Input, Output
import time


app = Dash(__name__, external_stylesheets=[
           dbc.themes.BOOTSTRAP], url_base_pathname='/self_serve/')
server = app.server

reset_flag = False
counter = 0
lockfilename = 'lockfile.txt' # this is where we keep the lock
with open(lockfilename, 'w') as fw:
    fw.write('0') # unlock when we start

app.title = 'jammed dash app'
app.layout = html.Div([
    # buttons
    dcc.Input(id='main', placeholder='Main Value'),
    dcc.Input(id='filter1', placeholder='filter 1'),
    dcc.Input(id='filter2', placeholder='filter 2'),
    dcc.Input(id='filter3', placeholder='filter 3'),
    dcc.Input(id='filter4', placeholder='filter 4'),
    html.Div(id='output', children='')
])

# metric list


@app.callback(
    Output(component_id='filter1', component_property='value'),
    Output(component_id='filter2', component_property='value'),
    Output(component_id='filter3', component_property='value'),
    Output(component_id='filter4', component_property='value'),
    [
        Input(component_id='main', component_property='value')
    ]
)
def update_filter(main):
    # clear up all filters if main is provided
    global reset_flag
    reset_flag = True
    return '', '', '', ''


@app.callback(
    Output(component_id='output', component_property='children'),
    [
        Input(component_id='main', component_property='value'),
        Input(component_id='filter1', component_property='value'),
        Input(component_id='filter2', component_property='value'),
        Input(component_id='filter3', component_property='value'),
        Input(component_id='filter4', component_property='value'),
    ]
)
def calculate(*args):
    # do some intensive calculation based on the inputs, but I do not want the clearing action to trigger this callback undesirably

    ctx = dash.callback_context
    print('\n')
    print('************ inside calculate *************')
    print('triggered:', ctx.triggered)
    print('inputs:', ctx.inputs)

    with open(lockfilename, 'r') as fr:
        line = next(fr)
        if(line[0] == '1'):
            print('Calc is locked, early exit')
            return dash.no_update

    # below is some intensive calculation using pandas.read_sql(), substituted by time.sleep()
    print('Starting heavy calc, locking the file')
    with open(lockfilename, 'w') as fw:
        fw.write('1')

    print('Wait 10 seconds here')
    for n in range(10):
        print('.', end='')
        time.sleep(1)

    output = ''
    for item in args:
        if item:
            output += item
    print('output:', output)
    print('Done with heavy calc, unlocking the file')
    with open(lockfilename, 'w') as fw:
        fw.write('0')

    return output


if __name__ == '__main__':
    app.run_server(debug=True)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM