1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Streamlitの再実行を止めたい?session_state / @st.cache_data / @st.cache_resource の使い分け

Posted at

user:

Streamlit アプリは、ユーザが何かするたびに再計算しますが、再計算せずに済ます箇所を指定しておくことはできますか?

assistant:

はい、Streamlit では「再計算(再実行)」を制御して、不要な再実行を防ぐ方法がいくつか用意されています。完全に再実行を止めることはできませんが、計算のキャッシュステート管理を使って、必要な部分だけ効率よく処理することができます。


✅ 方法1:@st.cache_data@st.cache_resource を使う

特定の処理結果をキャッシュして、同じ入力なら再実行せずキャッシュを使うようにできます。

import streamlit as st

@st.cache_data
def expensive_computation(x):
    st.write("処理中…")  # 再実行されたかどうか確認
    return x ** 2

value = st.number_input("数値を入力", value=1)
result = expensive_computation(value)
st.write("結果:", result)

🔁 同じ数値を入力し続ける限り expensive_computation() は再実行されません。


✅ 方法2:st.session_state を使う

Streamlit はユーザ操作で全スクリプトを上から再実行するため、値を保持したい場合は st.session_state を使います。

import streamlit as st

if "count" not in st.session_state:
    st.session_state.count = 0

def increment():
    st.session_state.count += 1

st.button("カウントアップ", on_click=increment)
st.write("現在のカウント:", st.session_state.count)

➡️ on_click 経由で関数を呼ぶことで、必要最小限の再実行に抑えることができます。


✅ 方法3:st.button, st.form, st.expander などの構造を活用

Streamlit では、フォーム (st.form) 内の入力が「Submit」されたときだけ再実行する構造が使えます。

with st.form("my_form"):
    name = st.text_input("名前を入力")
    submitted = st.form_submit_button("送信")
    if submitted:
        st.write(f"こんにちは、{name}さん!")

補足:再実行自体は止められないが「計算量の重い処理」は避けられる

Streamlit の設計上、何か変わると スクリプト全体を再実行します(例:ボタン、入力、選択肢などの変更)。
ただし、上記の方法を使えば、重い処理やデータロードなどを賢くスキップできます。


まとめ

方法 説明
@st.cache_data / @st.cache_resource 重い処理の結果をキャッシュする
st.session_state 変数の値を保持して、必要なタイミングでのみ変更
st.form ユーザの「送信」をトリガーに再実行
on_click 引数 ボタンクリック時のみ関数実行

必要であれば、あなたのアプリに応じた最適な再計算抑制パターンを一緒に設計できますよ。お気軽にご相談ください。

user:

ありがとうございます。方法1を試したら、次のエラーが出ました。解決法はたぶん分かりますが、エラーの内容を詳しく説明していただけますか?

streamlit.runtime.caching.cache_errors.UnhashableParamError: Cannot hash argument 'columns' (of type pandas.core.indexes.base.Index) in ‘function’.
To address this, you can tell Streamlit not to hash this argument by adding a leading underscore to the argument's name in the function signature:
@st.cache_data
def function(_columns, ...):
    ...

assistant:

こちらこそ、詳細に共有いただきありがとうございます!

あなたが遭遇したエラー:

streamlit.runtime.caching.cache_errors.UnhashableParamError:
Cannot hash argument 'columns' (of type pandas.core.indexes.base.Index) in ‘function’.

このエラーは、Streamlit のキャッシュシステム(@st.cache_data)が引数の「ハッシュ化(内容の識別)」に失敗したことを意味します。以下、丁寧に解説します。


🔍 なぜこのエラーが起きたのか?

Streamlit の @st.cache_data は、関数の引数の内容を「ハッシュ化(要するに中身の識別)」して、同じ入力なら再実行しないようにしています。

しかし、一部のデータ型(特に pandas の Index や DataFrame)などは、「ハッシュ可能(hashable)」ではないため、ハッシュ化に失敗してエラーになります。

このエラーは、例えば以下のような関数をキャッシュしようとした場合に出ます:

import pandas as pd
import streamlit as st

@st.cache_data
def my_func(columns):  # columns が Index 型
    return list(columns)

df = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
my_func(df.columns)  # ← df.columns は pandas.Index 型

💡 解決策:Streamlit に「この引数はハッシュ対象外」と伝える

Streamlit は、関数の引数名の先頭にアンダースコア( _ )を付けると、その引数はハッシュ対象から除外します。

✅ 修正例:

@st.cache_data
def my_func(_columns):  # ← _columns にすることでキャッシュ対象外に
    return list(_columns)

df = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
my_func(df.columns)

これでエラーは解消され、my_func() はキャッシュされるようになります。_columns はキャッシュに使われないので、キャッシュのキーには影響しません。


⚠️ 注意点:「キャッシュ対象外」にすることの意味

アンダースコアをつけてキャッシュ対象外にするということは:

  • その引数が変わってもキャッシュは再実行されない(古い結果が返される)
  • したがって、その引数が関数の結果に影響しないときだけ使うべき

もし columns の中身が結果に影響するなら、次のような別の方法で対応するべきです:

✅ 別の方法:リストに変換して渡す

@st.cache_data
def my_func(columns_list):  # list は hashable
    return columns_list

df = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
my_func(list(df.columns))

こうすれば、キャッシュキーとして適切に扱われます(list はハッシュ可能)。


✅ まとめ

原因 pandas の Index 型などが @st.cache_data のハッシュ対象にできない
解決方法1 引数名の先頭に _ をつけて「キャッシュ対象外」にする
解決方法2 引数を list()tuple() などの「ハッシュ可能な型」に変換する
注意点 キャッシュ対象外にした引数は、値が変わってもキャッシュが更新されない

エラーの理由を納得いただけたら幸いです。もし columns が重要な引数なら、型変換による対応をおすすめします。他のキャッシュや最適化のご相談も歓迎です。

user:

ありがとうございます。@st.cache_data と @st.cache_resource はどう違いますか?

assistant:

どういたしまして!
@st.cache_data@st.cache_resource の違いは、**「何をキャッシュするか」**にあります。ざっくり言うと:


@st.cache_data

→ 「データ」をキャッシュするのに使います。

  • 対象:軽量なデータ処理、データフレーム、計算結果、APIレスポンスなど
  • スコープ:入力引数ごとにキャッシュされる
  • 再実行の条件:引数が変われば再実行される
  • 典型例:Pandas データ読み込み、集計処理など

例:

@st.cache_data
def load_data(path):
    return pd.read_csv(path)

@st.cache_resource

→ 「リソース(外部接続や重いオブジェクト)」をキャッシュするのに使います。

  • 対象:モデルの読み込み、大規模データの初期化、DB接続、APIクライアントなど
  • スコープ:アプリ起動中は1回だけ実行(引数に関係なく)
  • 再実行の条件:コードが変わらない限り再実行されない
  • 典型例:LLM、DB接続、学習済モデルなどの読み込み

例:

@st.cache_resource
def load_model():
    return SomeHeavyModel.load_from_disk("model_path")

📊 比較表

特徴 @st.cache_data @st.cache_resource
主な用途 データ、計算結果のキャッシュ モデル、接続、外部リソースのキャッシュ
再実行のトリガー 引数が変わったとき コードが変わったとき
キャッシュの単位 引数に応じた複数バージョン 1回のみの初期化(singleton 的)
典型的な使い方 CSV読込、集計処理 モデル読込、DBコネクション

✅ 実際どう使い分ければいいの?

  • データ処理や計算 → @st.cache_data
  • 重い初期化(モデルや接続) → @st.cache_resource

✨ヒント:

  • データ処理で st.cache_resource を使うと、引数が変わっても結果が変わらず、意図しないキャッシュミスが起こることがあります。
  • モデルなどで st.cache_data を使うと、毎回ロードし直して無駄に遅くなることがあります。

💡補足:従来の @st.cache との違い

Streamlit 1.18 以降では、古い @st.cache は非推奨で、以下のように用途ごとに分かれました:

  • @st.cache_data(データ用)
  • @st.cache_resource(リソース用)

必要であれば、あなたのアプリの具体的なケースに合わせて「どっちを使うべきか」ご提案しますよ!

user:

ありがとうございました。今日はこの辺りで大丈夫です。ここまでの内容をブログにまとめたいので、タイトル案をいくつか提案していただけませんか?

assistant:

もちろんです!今日のStreamlitに関するキャッシュ・再実行制御の話をもとに、以下のようなブログタイトル案を提案します:


🔹 再実行の最適化にフォーカスしたタイトル

  1. 「Streamlitの再実行を止めたい?キャッシュとステートで効率化する方法」
  2. 「Streamlitで毎回再計算されて困る人へ:@st.cache_data と session_state の使い分け」
  3. 「Streamlitアプリ高速化の基本:再実行の仕組みとキャッシュの賢い使い方」

🔹 @st.cache_data / @st.cache_resource の使い分けにフォーカス

  1. 「@st.cache_data と @st.cache_resource を使いこなす:Streamlit最適化ガイド」
  2. 「Streamlitキャッシュの正しい使い方:データ vs リソースのベストプラクティス」
  3. 「なぜその関数は毎回実行されるのか?Streamlitのキャッシュで解決!」

🔹 初学者にも優しい語り口のタイトル

  1. 「Streamlitの“毎回全部再計算”問題をどう防ぐ?今日からできる対策まとめ」
  2. 「Streamlit初心者がつまずく“再実行地獄”から抜け出す3つの工夫」
  3. 「Streamlitのキャッシュ戦略:最小限の再計算でサクサク動くアプリを作る」

ご希望のトーン(技術寄り/初学者寄り/SEO重視など)があれば、それに合わせて再提案もできますので、お気軽にどうぞ!

user:

「Streamlitの再実行を止めたい?session_state / @st.cache_data / @st.cache_resource の使い分け」で行きたいと思います。最後に、詳しいまとめを書いてください。

assistant:

素晴らしいタイトルです!
では、ブログ記事の最後にふさわしい詳しいまとめをご用意しました👇


📝 まとめ:Streamlitの再実行をどう制御するか?

Streamlit は「ユーザーの操作があるたびにスクリプトを上から再実行する」という設計が特徴ですが、毎回全処理が動くと非効率になります。そこで重要になるのが、再実行を制御する3つの基本機能です。


✅ 1. st.session_state:状態保持の基本

  • ユーザーの操作や関数呼び出しで変数を保持する方法。
  • ボタンクリックで値を変更し、それ以外は再実行しないなど、意図的なトリガーの制御に使える。
  • 初期化条件の管理や状態遷移の実装にも便利。
if "count" not in st.session_state:
    st.session_state.count = 0

def increment():
    st.session_state.count += 1

st.button("カウントアップ", on_click=increment)

✅ 2. @st.cache_data:軽量なデータ・処理結果のキャッシュ

  • Pandas の read_csv や軽い前処理関数など、入力に応じて変化する計算処理に最適。
  • 同じ引数なら結果を再利用し、重い再計算を避けられる。
  • ただし、非ハッシュ可能な引数(例:pd.Index)には注意。
@st.cache_data
def load_data(path):
    return pd.read_csv(path)

✅ 3. @st.cache_resource:モデルやDBなど重いリソースのキャッシュ

  • 機械学習モデルや外部API接続、DB接続などの初期化コストが高いリソースを一度だけ読み込む用途。
  • 引数に関係なく、アプリ実行中は一度だけ実行される
  • 基本的に「設定が変わらない前提の処理」に使う。
@st.cache_resource
def load_model():
    return SomeModel.load("model.pt")

🔄 どれを使うべきか?

処理内容 使うべきもの
ユーザの操作状態管理 st.session_state
入力に応じた軽い処理 @st.cache_data
モデルや外部接続など @st.cache_resource

⚠️ 注意点:キャッシュの「効きすぎ」にも注意!

  • @st.cache_data@st.cache_resource は、結果がキャッシュされている間は更新されないため、値が変わらないのに動作がおかしいように見えることもあります。
  • その場合は clear_cache() や関数定義の変更などで再キャッシュを促す必要があります。

🎯 最後に

Streamlit の「毎回全再実行」という性質は、うまく使えば非常に直感的で強力ですが、計算量やユーザビリティを意識した制御が不可欠です。
今回紹介した st.session_state@st.cache_data@st.cache_resource の3本柱を使いこなして、サクサク動く実用的なアプリを作っていきましょう!


必要に応じて、コードスニペットや応用例などを記事に追加するのもおすすめです!仕上げのサポートがあれば、いつでもどうぞ。

user:

ありがとうございました!また次回もよろしくお願いします!

assistant:

こちらこそ、ありがとうございました!
いつでもお気軽にご相談ください。次回も楽しみにしております 😊
良い執筆&開発を!✍️🚀

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?