@app.callback improvements? (Trigger component, same Output multiple times, callback without Output, serverside store / callback cache)

Before commenting, i would like to clarify a few details. In my current implementation, i am creating one (key,value) pair per output, not one per callback. There are a few reasons for this choice

  • If makes it possible to write different outputs from the same callback to different stores (e.g. large outputs to files, smaller ones to a redis cache)
  • If only a single (key,value) per is written for a multi output callback, any callback that has any of the outputs as input will need to read the data associated with all outputs. By writing a (key,value) pair per output, this overhead can be avoided

As i noted previously, my intention of the memoize keyword in this context is to reuse the server side caching which is already carried out. However, this will only work for the ServersideOutput (which is being cached on the server) and not for the Output (which is not cached anywhere).

Another option would be simply to use the @flask_caching.memoize decorator, but the downside is that you would then be caching the data twice. Depending on the size of the data, this could be a big problem (big data) or no problem at all (small data).

EDIT: I guess one way to solve the problem would be to cache all outputs of a callback whenever the memoize keyword is used. The ServersideOutputs should of course be written to whatever backend specified, while the Outputs would need some kind of default backend.

The defaults for output storage (i.e. backend and session_check) could be configured via the app, i.e something like

app = Dash(prevent_initial_callbacks=True, output_defaults=dict(backend=FileSystemStore(), session_check=True)

It would then be possible to mix Output and ServersideOutput in a memoized callback. You could even do an Output only callback with memoize=True,

@app.callback([Output("store", "data"), Output("log", "children")], Trigger("btn", "n_clicks"), memoize=True)
def query_data():
    time.sleep(1)
    return px.data.gapminder().to_json(), "Data loaded"

which would yield the exact same behaviour as the @flask_caching.memoize decorator.

3 Likes

I have just pushed a new version of dash-extensions (0.0.26) that implements the ideas above. Suggestions for syntax changes are still very welcome, but i think it is already pretty neat :slight_smile:

Another improvement for the Input/State dependencies would be the ability to specify the resulting argument name in the dependency itself, the argument would then be passed to the function as named arguments (instead of positional arguments), the order of the Input/State dependency would then be irrelevant.
The same could be done with the Output dependencies (giving them a name) and allowing callbacks to return a dict instead of a value (if mono output) or a tuple (if multiple output).

E.g. Input("my-button", "nclicks", name=btn_clicks) will pass an argument named btn_clicks to the callback function.
The callback function could then also accept **kwargs if useful.
With an Output("my-graph","figure", "my_fig"), the callback would return a dict(my_fig=...).
Using named inputs/outputs would make adapting the callback (adding/removing/moving dependencies) less troublesome.

An alternative (which would also be backward compatible) to adding a name argument to the dependency would be to pass a dict to the callback decorator like

@app.callback({"btn_clicks":Input("my-button", "nclicks"), ...})
def mycallaback(btn_clicks): ...

Or even named arguments like

@app.callback(btn_clicks=Input("my-button", "nclicks"), ...)

Yet, when passing a dict (instead of adding a name argument to Dependency), we should specify a dict for Input/State (goes into callaback signature) and a dict for Output (used for mapping the return value) as the same key name could be used as Input/State as well as Output.

Hi @chriddyp, ?
Reading the link you provide re DAG mentions that a DAG has no cycles.
Could you elaborate on why multiple callbacks outputting to the same component breaks the DAG/no cycle property?