Claude Media
Claude CodeでPlotly Dashのコールバックを直す — 動かない・遅いの切り分け

Claude CodeでPlotly Dashのコールバックを直す — 動かない・遅いの切り分け

Dashのコールバックが動かない・遅いときの原因を、Input/Output/Stateの取り違え、初回呼び出し、キャッシュの順にClaude Codeへ切り分けさせる手順です。

Dashのコールバック不具合をClaude Codeに任せる流れ

Dashのコールバックが動かないときは、症状ごとに原因の層が違います。出力が変わらないなら宣言の取り違え、起動直後に余計な処理が走るなら初回呼び出し、操作のたびに待たされるなら重い計算が候補です。この記事は、この三つを順に潰す指示の出し方をまとめます。

Plotly Dashは、PythonだけでブラウザーのWebアプリを組み立てるフレームワークです。画面の部品(コンポーネント)をapp.layoutに並べ、部品どうしの連動は@callbackで書きます。コールバックとは、入力側の部品のプロパティが変わるたびに自動で呼ばれ、出力側の部品のプロパティを更新する関数のことです。

Claude Codeに任せる利点は、原因の仮説を立てて実際に動かし、結果を見てコードを直す反復を肩代わりしてくれる点です。ただし「Dashが動かない」と伝えるだけでは、Claude Codeも推測で書き換えることになります。再現コマンドと症状を渡すと、直す範囲が絞れます。

切り分け

症状から入口を選ぶ

  1. Q操作しても出力が変わらない、またはエラーが出ますか?
    答えが次の問いへ

    宣言の取り違えを疑います。次の節に進みます。

    答えがはい
  2. Q起動直後やページ遷移直後に、余計な処理が走りますか?
    答えが次の問いへ

    初回呼び出しを疑います。prevent_initial_callの節に進みます。

    答えがはい

操作のたびに待たされるなら重い計算

キャッシュの節に進みます。

再現コマンドと症状を渡して原因を絞る

最初の指示は、Claude Codeの公式ワークフローにあるバグ修正の型に沿います。エラーメッセージを共有し、修正案を出してもらい、適用する、という三段です。公式の補足には、再現コマンドとスタックトレースを渡すこと、再現手順を伝えること、毎回起きるのか断続的なのかを伝えることが挙がっています。

Dashに当てはめると、渡す材料は次のとおりです。

  • 起動コマンド(例: python app.py)と、ブラウザーの操作手順
  • ターミナルに出たエラーと、ブラウザーの開発者ツールのコンソールに出たエラー
  • 期待した動作と実際の動作(「ドロップダウンを変えてもグラフが変わらない」など)
python app.py で起動し、ドロップダウンを変えてもグラフが更新されません。
エラーはターミナルに出ていません。まず修正せず、callback の Input / Output /
State の宣言と app.layout の id を突き合わせ、食い違いを一覧にしてください。

「まず修正せず」と添えるのが要点です。いきなり書き換えさせると、原因でない箇所まで触られて差分が読みにくくなります。一覧を見てから「このうち2番目だけ直して」と絞ると、レビューも楽になります。

Input・Output・Stateの取り違えを潰す

コールバックの宣言は、次の決まりを外すと動きません。Dashの基本ドキュメントにある条件を、Claude Codeへの確認項目に使えます。

確認項目外したときの症状
@callbackの直下に関数定義が続いている外したときの症状空行が挟まるとコールバックの登録に失敗する
宣言に書いたidがapp.layoutのidと一致している外したときの症状部品に結びつかない
関数の引数が、Input、Stateの順で宣言と同じ並びになっている外したときの症状値が別の部品のものとして渡る
dash.dependencies.Inputとdcc.Inputを混同していない外したときの症状前者は宣言用で、後者は実際の入力部品

引数の並びは、公式に「既定では位置引数で、先にInput、続いてStateが宣言と同じ順で渡される」とあります。宣言を入れ替えたのに関数の引数を直し忘れると、エラーなしで値だけがずれるので、見つけにくい型です。

Stateを使い分ける場面

Stateは「値は読むが、変化では呼ばれない」という指定です。フォームのように、入力途中ではなく送信ボタンを押した時点でまとめて読みたい場面に向きます。公式の例では、2つのテキスト入力をStateにして、ボタンのn_clicksだけをInputにしています。

@callback(
    Output("output-state", "children"),
    Input("submit-button-state", "n_clicks"),
    State("input-1-state", "value"),
    State("input-2-state", "value"),
)
def update_output(n_clicks, input1, input2):
    return f'Input 1 is "{input1}" and Input 2 is "{input2}"'

入力のたびに重い計算が走って困る場合は、Inputになっている入力部品の一部をStateに変え、ボタンをトリガーにする方向で、Claude Codeに作り直しを頼めます。

前と後

入力のたびに走る計算を、ボタン押下に寄せる

BEFORE

入力部品がすべてInput

テキストを1文字打つたびにコールバックが呼ばれ、計算が走ります。

AFTER

入力部品をStateにして、ボタンだけをInputにする

ボタンを押した時点で、入力値をまとめて読んで計算します。

宣言の確認をClaude Codeの規約にする

毎回同じ確認を頼むなら、CLAUDE.mdに書いておくのが手です。CLAUDE.mdは、プロジェクトへの永続的な指示を書くマークダウンで、Claude Codeがセッション開始時に読みます。次は、この記事の確認項目をそのまま規約にした例です。

## Dash のコールバック規約
- @callback の直下に関数定義を置く(空行を挟まない)
- 宣言の id は app.layout の id と一致させる
- 関数の引数は Input、State の順で、宣言と同じ並びにする
- コールバックを変えたら app.py を起動して、操作と結果を報告する

最後の1行が効きます。起動して操作した結果まで報告させると、コードを読んだだけの「直したはず」で終わりません。

起動時の余計な呼び出しをprevent_initial_callで止める

Dashのコールバックは、アプリの起動時に、入力部品の初期値で一度ずつ呼ばれます。これが「初回呼び出し」です。出力部品のレイアウトにchildren='Hello world'のような初期値を書いても、起動時に上書きされます。「書いた初期値が消える」症状の正体はこれです。

prevent_initial_call=Trueの効き方

初回の実行を止めたいコールバックには、prevent_initial_call=Trueを付けます。

@callback(
    Output("output", "children"),
    Input("input", "value"),
    prevent_initial_call=True,
)
def update_output(value):
    return value

この属性が止めるのは、アプリのレイアウトが最初に読み込まれたときと、コールバックの結果として新しい部品がレイアウトに加わったときの実行です。

ただし、公式が注意を促している例外があります。コールバックの入力部品が、別のコールバックによって後からレイアウトに挿入される場合、出力部品がすでにレイアウトにあると、prevent_initial_callでは止まりません。出力も一緒に挿入されるときだけ止まります。ページ遷移ごとに中身を差し替える構成(dcc.Locationと組み合わせる型)では、この例外に当たりやすくなります。

一部の出力だけ更新を止める

条件によって更新したくないときは、コールバックの中で止める手もあります。PreventUpdateを送出すると、そのコールバックのすべての出力が更新されません。dash.no_updateを返すと、返した出力だけが更新されず、他の出力は更新されます。

from dash import no_update
from dash.exceptions import PreventUpdate
 
def callback_body(n_clicks, text):
    if n_clicks is None:
        raise PreventUpdate      # すべての出力を更新しない
    if not text:
        return no_update, "入力が空です"  # 1つ目の出力だけ更新しない
    return text.upper(), ""

Claude Codeに頼むときは、「起動直後に走る処理を洗い出し、止めるべきものにprevent_initial_call=Trueを付けて、起動時に何回呼ばれるかをログで示して」と具体的に書きます。呼び出し回数のログを出させると、効果を数で確かめられます。

落とし穴

入力を後から挿入する構成で効かない

出力部品が先にレイアウトにあり、入力部品が別のコールバックで後から挿入される構成では、prevent_initial_call=Trueを付けても初回に走ります。出力と入力を同じコールバックの戻り値に入れて一緒に挿入するか、PreventUpdateで中から止めます。

重い計算はキャッシュで減らす

操作のたびに待たされるなら、公式の指針では、アプリの速度を左右するのは多くの場合コールバックそのものです。コールバックが速くなれば、アプリも軽快に感じられます。

まず計測して、重い関数を特定する

キャッシュを足す前に、どの関数が遅いのかを測ります。Claude Codeには計測を先に頼みます。

遅いと感じる操作は「期間を変更」です。コールバックの処理を
time.perf_counter で区間ごとに計測し、データ読み込み・集計・グラフ生成の
どこに時間がかかっているか、表で示してください。まだ修正はしないでください。

表の結果を見て、キャッシュするのは時間のかかる区間の関数に絞ります。コールバック全体をキャッシュすると、メモリーを余計に使います。公式も、コールバックそのものではなく、コールバックから呼ぶ自前の関数をキャッシュするほうが、メモリー効率が良い場合があると述べています。

キャッシュの選択肢

Dashの公式ドキュメントが挙げる選択肢は次のとおりです。

方法向く場面注意点
functools.lru_cacheなどのメモリー内キャッシュ向く場面単一プロセスの小さな検証注意点複数のプロセスやスレッドで動かすと、各プロセスが別のメモリーを持つため、結果が共有されない
Flask-Caching(Redisまたはファイル)向く場面複数プロセスで結果を共有したい、一定時間で更新したい注意点Flaskバックエンド専用。FastAPIやQuartなら別の仕組みを使う
バックグラウンドコールバックのキャッシュ向く場面計算をアプリサーバーから切り離したい注意点計算がバックグラウンドのジョブキューに移る。詳細はBackground Callbacksの章

Flask-Cachingなら、timeoutを秒で指定でき、時間切れで古い結果を捨てられます。毎時や毎日でデータを更新したいときに役立つ、と公式にあります。ファイルシステムを使う場合の設定は、次のとおりです。

from flask_caching import Cache
 
cache = Cache(app.server, config={
    "CACHE_TYPE": "filesystem",
    "CACHE_DIR": "cache-directory",
})
 
@cache.memoize(timeout=300)  # 秒
def load_dataset(name):
    ...  # 重い読み込み

Claude Codeには「load_datasetだけをキャッシュし、コールバックの入力が同じなら2回目以降は読み込みが走らないことを、実行時間で示して」と頼みます。キャッシュの効果は、同じ操作を2回続けたときの時間差で見えます。

分割できる計算は別のコールバックにする

複数の出力が同じ入力を使う場合でも、計算の中身が大きく違うなら、コールバックを分けると並列に走らせられます。逆に、同じ重い計算を共有する出力を別のコールバックに分けると、同じ計算を二度走らせることになります。Claude Codeに分割を頼むときは、この二つの基準を渡して、どちらに当たるかを判断させます。

直した結果をテストで固める

Claude Codeの公式ワークフローには、テストを追加して実行し、失敗を直す流れがあります。Dashでは、コールバックの本体を、部品の宣言から切り離した関数に抜き出しておくと、テストを書きやすくなります。

def build_summary(rows, threshold):
    """コールバックから呼ぶ純粋な関数。部品に依存しない。"""
    return [r for r in rows if r["value"] >= threshold]
 
@callback(
    Output("table", "data"),
    Input("threshold", "value"),
)
def update_table(threshold):
    return build_summary(load_rows(), threshold)

build_summaryのような関数なら、Dashを起動せずにテストを実行できます。pytestの書き方はClaude CodeにpytestのCLAUDE.md規約を教えるにまとめています。直したあとに実行を自動化したい場合は、Claude Code hooksでテストを自動実行するの使い分けが参考になります。

コールバックのうち、画面の連動そのものは、起動して操作しないと確かめられません。画面の操作確認は人が行い、コールバックの中身の計算はテストで固める、という分担にすると、直したところが後で壊れにくくなります。

同じPythonのダッシュボードでも、Streamlitは再実行型の別の仕組みです。そちらの作り方はClaude CodeでStreamlitアプリを作る手順で扱っています。処理の遅さがメモリーの増加として現れる場合は、Claude CodeでPythonのメモリーリークを特定するの計測手順が使えます。

まとめ

Dashのコールバック不具合は、宣言、初回呼び出し、計算コストの順に層を分けると、原因を一つずつ消せます。Claude Codeには、再現コマンドと症状を渡し、「まず修正せず一覧にして」と頼むのが入口です。

初回呼び出しはprevent_initial_callだけで足りない構成があるので、入力が後から挿入されるかどうかを先に確かめます。キャッシュは、計測で重いと分かった自前の関数に絞り、複数プロセスで動かすなら共有できる仕組みを選びます。直した結果は、CLAUDE.mdの規約とテストで固めておくと、次の変更で同じ不具合に戻りにくくなります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. dash.plotly.com/basic-callbacks
  2. dash.plotly.com/advanced-callbacks
  3. dash.plotly.com/performance
  4. code.claude.com/docs/en/common-workflows
  5. code.claude.com/docs/en/memory
この記事を共有:XはてブLinkedIn