Как избежать запуска нежелательных обратных вызовов в Python Dash?

#python #plotly-dash

#python #plotly-dash

Вопрос:

Я написал приложение Dash, исходный код которого приведен ниже:

 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)
 

Мне нужно выполнить некоторые интенсивные вычисления (например, код sql) во 2-м обратном вызове («вычислить»). Я также хотел бы очищать все элементы фильтра всякий раз, когда изменяется значение в «main», что реализовано в 1-м обратном вызове («update_filter»).
Проблема в том, что, когда каждый фильтр очищается при 1-м обратном вызове, dash быстро вызывает 2-й обратный вызов, и программа застревает.

Мой вопрос: как избежать запуска 2-го обратного вызова при вызове 1-го обратного вызова? Моя идея состоит в том, чтобы отслеживать, сколько раз 2-й обратный вызов вызывается «избыточно» и позволяет передавать «правильный» вызов, возвращая пустой вывод для «избыточных» вызовов. Я пытался реализовать использование глобального счетчика, но это безрезультатно.

И вообще, какова наилучшая / обычная практика для предотвращения этих нежелательных, связанных обратных вызовов?

Заранее большое спасибо!

Комментарии:

1. Я не уверен, какова наилучшая практика, но я бы поместил кнопку Calculate в ваш макет и запустил бы ваши дорогостоящие вычисления только тогда, когда пользователь нажимает на эту кнопку. Вы можете настроить свои фильтры и т. State Д. Так, Input Чтобы они были доступны в вашем обратном вызове, Но dot запускает вычисление, community.plotly.com/t /…

2. Да, я думал об этой идее. В этом случае только кнопка будет единственным входом, в то время как другие фильтры и main будут состоянием. Но, к сожалению, запрос заключается в том, чтобы попытаться избежать использования button и позволить вычислению выполняться динамически. В любом случае спасибо за обмен.

Ответ №1:

Вот моя довольно грубая реализация блокировки вашей calculate функции с использованием файла. Я думаю, что глобальные переменные проблематичны, учитывая различные проблемы с потоками и т.д. Идея заключается в том, что при calculate выполнении своих тяжелых вычислений он помещает ‘1’ в файл, а когда это делается, он помещает туда ‘0’. Если тем временем функция вызывается снова, она проверяет файл блокировки, и если там есть ‘1’, он существует без запуска пересчета.

 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)

 

Комментарии:

1. Привет, питербарг, спасибо, что поделились своими мыслями. Я полностью согласен с тем, что самая сложная часть заключается в проблемах с потоками. Возможно, мой примерный код (который я уменьшил чувствительность) не отражает реальную проблему. Дело в том, что всякий раз, когда поток программы переходит к «тяжелому вычислению», в реальном коде это pd.read_sql() , который выдает ошибку при вызове в быстрой последовательности, не позволяя ему возвращать фрейм данных. Я думаю, по сути, использование глобальной переменной в качестве счетчика и обновление «флага» в файле одинаковы. Основная проблема заключается в управлении потоками, чтобы обратный вызов выполнялся в контролируемой последовательности.

2. Еще раз привет, очень тонко, что моя логика оказалась правильной, но она работает не на моем компьютере, а на чужом (вероятно, из-за некоторых проблем с конфигурацией за пределами python). Сегодня я также перешел по этой ссылке community.plotly.com/t/sharing-a-dataframe-between-plots/6173 , который описывает ваш метод, а также некоторые другие альтернативы.

Ответ №2:

В вашем случае то, что вы, возможно, захотите сделать, это:

 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)  
 

Это не блокируется, потому что это цепной обратный вызов: изменение main очищается filter , а изменение filter запускает второй обратный вызов, который принимает main состояние и обновляет output .

Для трудоемких операций вы можете захотеть кэшировать эти результаты. В документе есть запись, показывающая это: https://dash.plotly.com/performance

Комментарии:

1. Так получилось, что я уже пробовал это. Но проблема в том, что, как только фильтр будет сброшен, filter запустит обратный вызов «вычисления». Таким образом, это все еще сложная ситуация. Но, установив для filter значение State вместо Input, любое обновление только в filter не вызовет вычисление. Это основная проблема. В любом случае спасибо, что поделились.

2. Я не думаю, что вы еще не прочитали вторую часть. Вы должны кэшировать свои результаты calculate , чтобы он не пересчитывал трудоемкую задачу. Это само по себе избавит от необходимости использовать счетчики и прочее.