DashというPythonのWebアプリケーションフレームワークで開発していて、問題解決に苦労したことがあったため、備忘録として残しておきます。
実装上の注意を一覧として記載するのではなく、発生した問題を一覧としてその原因を説明するという流れで記載します。
なお、今回使用したDashのバージョンは3.0.4 です。
コールバックが意図せず呼ばれる
原因
ボタンをクリックしていないのに、そのボタンクリックをトリガーとしたコールバックが呼ばれていました(根本的な原因は不明)。
@callback(
Output("display-area", "children"),
Input("button-a", "n_clicks")
)
def button_a_click(n_clicks):
return html.Div("ボタンがクリックされました。")
解決策
ボタンがクリックされたかどうかを判定する処理を追加します。
@callback(
Output("display-area", "children"),
Input("button-a", "n_clicks")
)
def button_a_click(n_clicks):
if n_clicks is None or n_clicks == 0:
raise PreventUpdate
return html.Div("ボタンがクリックされました。")
コールバックが呼ばれない①(CallbackException)
原因
以下のエラーがサーバのコンソールに出ていました。
dash.exceptions.CallbackException: Inputs do not match callback definition
コールバックの引数を変更した後、以前表示していたウェブページ上でそのままボタン押下したことが原因でした。
解決策
ブラウザでページをリロードするなど、更新後のコードを反映させる必要がありました。
コールバックが呼ばれない②(SchemaLengthValidationError)
原因
以下のエラーがサーバのコンソールに出ていました。
dash._grouping.SchemaLengthValidationError: Schema: [click-year.data>, click-month.data>, click-day.data>]
Path: ()
Expected length: 3
Received value of length 2:
コールバックのOutputとreturnの数が一致していないことが原因でした。
以下のように、3つのOutputを設置している一方で、returnはふたつしかないありませんでした。
@callback(
Output("click-year", "data"),
Output("click-month", "data"),
Output("click-day", "data"),
Input("button-a", "n_clicks")
)
def click_date(n_clicks):
now = datetime.now()
return now.year, now.month
正しい実装例
@callback(
Output("click-year", "data"),
Output("click-month", "data"),
Output("click-day", "data"),
Input("button-a", "n_clicks")
)
def click_date(n_clicks):
now = datetime.now()
return now.year, now.month, now.day
コールバックを実装する際には、「Outputの数がreturnの数と一致すること」だけでなく、「Inputの数とStateの数の和が引数の数と一致すること」が守れているか確認しておくとよいと思います。
コールバックが呼べない③(ReferenceError)
原因
サーバのコンソールにはエラーが出ていなく、ブラウザの開発者ツールで閲覧できるコンソールに以下のエラーが出ていました。
ReferenceError: A nonexistent object was used in an `State` of a Dash callback. The id of this object is `XXX` and the property is `data`.
以下のように、XXXというIDを持つ要素がページに存在しないので、エラーとなる。
app.layout = html.Div([
dcc.Button("ボタン", id="button-a"),
@callback(
Output("value-a", "data"),
Input("button-a", "n_clicks"),
State("XXX", "data")
)
def click_date(n_clicks, xxx):
return f"{xxx}: {n_clicks}"
コールバックを追加したら、関係のないコールバックまで動かなくなってしまった
原因
複数のコールバックで同じIDのOutputを、allow_duplicate=Trueなしで使用していたせいでした。
right_button_clickがなく、left_button_clickだけの場合は問題なく動きますが、right_button_clickを追加した以下のコードは動かないです。
@callback(
Output("display-area", "children"),
Input("button-left", "n_clicks")
)
def left_button_click(n_clicks):
return html.Div("左ボタンがクリックされました。")
@callback(
Output("display-area", "children"),
Input("button-right", "n_clicks")
)
def right_button_click(n_clicks):
return html.Div("右ボタンがクリックされました。")
正しい実装例
複数のコールバックで同じIDを使う際は、allow_duplicate=Trueが必要でした。
なお、allow_duplicate=Trueを使う場合、prevent_initial_call=Trueも必要になります。
@callback(
Output("display-area", "children", allow_duplicate=True),
Input("button-left", "n_clicks"),
prevent_initial_call=True
)
def left_button_click(n_clicks):
return html.Div("左ボタンがクリックされました。")
@callback(
Output("display-area", "children", allow_duplicate=True),
Input("button-right", "n_clicks"),
prevent_initial_call=True
)
def right_button_click(n_clicks):
return html.Div("右ボタンがクリックされました。")
エラーが出ていないのにコンポーネントが表示されない
原因
以下のように、不要なカンマがついていたことが原因でした。
カンマによって、compoがタプルとなってしまっていました。
compo = html.Div("表示したい文字"),
allow_duplicate=Trueを設定しているのに、duplicate outputsエラーが出る
原因
同じInputとOutputの組み合わせが、複数個所に存在していたことが原因でした。
@callback(
Output("out-id1", "data", allow_duplicate=True),
Input("in-id1", "data"),
prevent_initial_call=True
)
def func1(id1):
return "val1"
@callback(
Output("out-id1", "data", allow_duplicate=True),
Input("in-id1", "data"),
prevent_initial_call=True
)
def func2(id1):
return, "val2"
ちなみに、下のように、入出力全体を比較するとfunc1とfunc2で異なっていますが、out-id1とin-id1のペアは共通しているため、この場合でもエラーとなりました。
@callback(
Output("out-id2", "data"),
Output("out-id1", "data", allow_duplicate=True),
Input("in-id1", "data"),
State("st-id1", "data"),
prevent_initial_call=True
)
def func1(id1, st1):
return "val1", "val1"
@callback(
Output("out-id3", "data"),
Output("out-id1", "data", allow_duplicate=True),
Input("in-id1", "data"),
State("st-id2", "data"),
prevent_initial_call=True
)
def func2(id1, st2):
return "val2", "val2"
正しい実装例
それらの関数が同じトリガーの場合は、ひとつのコールバックにまとめます。
@callback(
Output("out-id1", "data"),
Output("out-id2", "data"),
Output("out-id3", "data"),
Input("in-id1", "data"),
prevent_initial_call=True
)
def func_common(id3, id1):
return "val1", "val2", "val3"
そうではなく、実はトリガー対象や更新対象が異なる場合、用途ごとにIDを用意するようにします。
私の場合はこのケースでした。複数機能で同じような構成のコンポーネントを作成したかったので、共通モジュールを作成しました。そのモジュール内でIDの要素を作成していたわけですが、処理ロジックが機能ごとに異なるため、コールバックは各機能側に実装しました。その結果、同じInputとOutputを持つコールバックが複数できてしまいました。
以下が実装例です。
# 共通モジュール内
def create_compo(id):
...
stores = [
dcc.Store({"type": "in-id1", "elem-id": id}),
dcc.Store({"type": "out-id1", "elem-id": id})
]
...
# 機能A用
compo_a = create_compo(id="A")
# 機能B用
compo_b = create_compo(id="B")
# 機能A用
@callback(
Output({"type": "out-id1", "elem-id": "A"}, "data")
Input({"type": "in-id1", "elem-id": "A"}, "data")
)
def func1(id1):
# 機能A用の処理
return "val1"
# 機能B用
@callback(
Output({"type": "out-id1", "elem-id": "B"}, "data")
Input({"type": "in-id1", "elem-id": "B"}, "data")
)
def func2(id1):
# 機能B用の処理
return "val2"
パターンマッチのコールバックを実装したらエラー
原因
以下のように、パターンマッチによるトリガーのコールバックに固定IDの要素を更新しようとしましたが、これは受け入れられないようです。
@app.callback(
Output("data-a-store", "data"),
Input({"elem-id": MATCH, "type": "data-a"}, "data"),
)
def update_store(value):
return value
このコードを使ったアプリを実行すると、以下のエラーが出ます。
MATCH wildcards must be on the same keys for all Outputs.
ALL wildcards need not match, only MATCH."
正しい実装例
パターンマッチによる各条件のOutputを用意し、一旦そこに入れます。
そして、ALLでそのOutputの変更を検知するコールバックを作成し、固定IDの要素を更新します。
@app.callback(
Output({"elem-id": MATCH, "type": "data-a-store"}, "data"),
Input({"elem-id": MATCH, "type": "data-a"}, "data"),
)
def update_local_store(value):
return value
@app.callback(
Output("data-a-store", "data"),
Input({"elem-id": ALL, "type": "data-a-store"}, "data"),
State({"elem-id": ALL, "type": "data-a-store"}, "id"),
)
def update_global_store(values, ids):
if not ctx.triggered:
raise PreventUpdate
# どのIDによって発火したか
trigger_id = ctx.triggered[0]["prop_id"].split(".")[0]
# 対象のIDに対応するデータを取得し、Outputに設定
for i, id in enumerate(ids):
if str(id) == trigger_id:
return values[i]
グローバル変数に値を設定したが、それが反映されない
原因
Dashのサーバー処理は、リクエスト間で同一プロセスで処理されるとは限りません。
最初のリクエストで呼ばれたコールバックがグローバル変数に値を設定しても、次のリクエストのコールバックは別のプロセスで実行される可能性があるので、更新された値を参照できません。
解決策
ユーザーに見られても問題ない場合は、dcc.Storeに格納すれば、コールバック間でデータを共有できます。
ユーザーに見られたくない情報は、サーバサイドで完結する仕組み(例えばデータベースを用意したり、ファイルに書き出したり)が必要となります。
gを使ったら、RuntimeError: Working outside of application context.
原因
gを生成AIが使用する関数で参照していたことが原因でした。作成したPython関数を生成AIが使えるようにするTool Useの機能を使っていたときに発生しました。
生成AIというのは一例となりますが、使用しているライブラリが内部でマルチプロセスとして動作する場合gが使えないようです。
解決策
gを使った解決策は見つからなかったので、gを使わないで実装することにしました。
gについて補足
gはFlaskのグローバルコンテキストオブジェクトで、リクエスト処理の間だけ有効な一時的なデータを保存するためのものです。
以下のように、リクエスト処理の開始時に任意の値を設定しておくと、コールバックなどで参照することができます。
from flask import g
@server.before_request
def before_request_func():
g.user = get_current_user()
定期的に呼ばれるコールバックで処理結果が反映されないことがある
原因
dcc.Intervalで定期的にコールバックを実行していました。このコールバックは、InputとOutputが同じで(例えば、値をインクリメントするなど)、処理時間が長いものでした。
そのため、1回目のコールバック呼び出しが完了する前に2回目のコールバックが呼ばれていました。
その結果、1回目と2回目のコールバックが同じ引数を受け取っていたことがわかりました。
app.layout = html.Div([
dcc.Store(id="counter-store", data=0),
# 1秒間隔で発火する
dcc.Interval(id="interval", interval=1000, n_intervals=0)
])
@callback(
Output("counter-store", "data"),
Input("interval", "n_intervals"),
State("counter-store", "data"),
prevent_initial_call=True
)
def update_counter(n_intervals, current_count):
# funcが1秒以上かかると、同じcurrent_countでfuncが複数回呼ばれる
func(current_count)
return current_count + 1
解決策
解決策のひとつとして、二段階でコールバックを用意します。
二段目で実際にOutputを更新するのですが、一段目は二段目が実行中かどうかのみを判定し、実行中でない場合のみ二段目のコールバックを発火させるように制御します。
app.layout = html.Div([
dcc.Store(id="counter-store", data=0),
dcc.Store(id="processing-flag", data=False), # 処理中フラグの追加
# 1秒間隔で発火する
dcc.Interval(id="interval", interval=1000, n_intervals=0)
])
# 第一段階:処理中でなければ第二段階をトリガー
@callback(
Output("processing-flag", "data", allow_duplicate=True),
Input("interval", "n_intervals"),
State("processing-flag", "data"),
prevent_initial_call=True
)
def check_and_trigger(n_intervals, is_processing):
if not is_processing:
return True # 第二段階をトリガーするためにフラグを立てる
raise PreventUpdate
# 第二段階:実際の処理とカウントアップ
@callback(
Output("counter-store", "data"),
Output("processing-flag", "data"),
Input("processing-flag", "data"),
State("counter-store", "data"),
prevent_initial_call=True
)
def perform_increment(is_processing, current_count):
# 処理の代わりに重い関数を模擬(例:3秒かかる)
def func(x):
time.sleep(3)
return x + 1
new_count = func(current_count)
return new_count, False # 処理完了後、フラグを下ろす
コールバックを書き換えたが、ページを再ロードしても更新されない
原因
サーバ側のプロセスを再起動したとしても、(おそらく)ブラウザに更新前のコールバックがキャッシュされていてそれが実行された模様。
この状況が発生したのは、コールバックの修正として、入出力の要素IDを書き換えただけのときでした。
以下のように、コールバックの入力を変更したがその変更が反映されず、YYYの値を参照したかったがXXXの値が参照されてしまいました。
変更前:
@callback(
Output("value-a", "data"),
Input("button-a", "n_clicks"),
State("XXX", "data")
)
def click_date(n_clicks, xxx):
return f"{xxx}: {n_clicks}"
変更後:
@callback(
Output("value-a", "data"),
Input("button-a", "n_clicks"),
State("YYY", "data")
)
def click_date(n_clicks, xxx):
return f"{xxx}: {n_clicks}"
解決策
正攻法は不明ですが、簡単にキャッシュを削除する手段として、今回は以下の手順で動作確認を行いました。
まずブラウザのシークレットモードで操作を使って操作することにしました。
もし、コールバックを変更したら、シークレットモードのブラウザウィンドウを閉じて、再度開きなおします。
もし、複数のシークレットモードのウィンドウがあれば、すべてを閉じる必要があります。
その他、注意すること
- dcc.Stateでデータを保存できるが、IDは画面内で一意になるようにする。
- バックエンドコールバックマネージャー用のコールバックで、プログレスを設定する関数(set_progress)の位置は先頭。
@callback( Output("output-a", "data"), Inupt("input-a", "data"), State("state-a", "data"), progress=[Output("progress_value", "data")], background=True ) def bgcallback(set_progress, input_a, state_a)
おわりに
Dashを使ったWEBアプリ開発していて、問題が発生した時は(1)サーバのコンソールや(2)ブラウザの開発者ツールのコンソールにエラーメッセージが出力されることがありますが、具体的な問題個所の特定が難しかったり、エラーメッセージが出力されないこともあります。
同じ状況でも同じ原因とは限りませんが、このメモが原因究明の助けになれば幸いです。