Streamlit Core (1.58.x) — 実行モデル、状態管理、更新 は何をしますか?
唯一のメンタルモデル: あらゆるユーザーインタラクション(いずれかのウィジェット操作)のたびに、Streamlit はスクリプト全体を 上から下まで 再実行します。イベントハンドラ、永続的なローカル変数、メインループは存在しません。1回の実行は st.session_state + 現在のウィジェット値の純粋関数です。以下のすべてはこの原則に基づいています。
すべての主張を実行可能な証明: このスキルフォルダ内の st_core_lab.py(python -m streamlit run ~/.roo/skills/st-core/st_core_lab.py)。
Streamlit 1.58 で検証済み、API は 1.55 で確認: このスキルの内容は両方のバージョンに存在します(st.fragment(run_every=) は ≥1.37 が必要)。
厳格なルール(これに違反すると古典的な壊れたアプリになります)
- スクリプトレベルで決して
while True:を使わない。 スクリプトは各実行を完了しなければなりません。ライブセクションには@st.fragment(run_every="2s")を使用します(後述)。有限のジョブ(プログレスバー、st.write_stream)のためのループは、1回の実行の 内部でのみ 問題なく使用できます。 st.rerun()を無条件に(または次の実行でも真である条件下で)呼び出してはいけない — Streamlit にはループ保護機能が なく、無限に回り続けます。すべてのst.rerun()は、事前にフリップする状態ガードの後ろに配置しなければなりません:if st.session_state.get("needs_refresh"): st.session_state.needs_refresh = False # rerun の前にフリップ refresh_data() st.rerun()- 自動更新のために
time.sleep(n); st.rerun()を決して使わない。 ページが安定しません: ウィジェットがちらつき、スリープ中にクリックが無視されます。@st.fragment(run_every=...)を使用するか、「更新」が単により新しいデータを意味するのであればst.cache_data(ttl=...)を使用してください。ネイティブのst.autorefreshは存在しません(確認済み —dir(st)には何もありません)。streamlit-autorefreshPyPI パッケージはサードパーティ製で、ティックごとにスクリプト全体を再実行します(すべてのウィジェットをリセットし、フォーカスを奪います)。ネイティブのスコープ付き@st.fragment(run_every=...)を優先してください。 st.experimental_rerun()は存在しません(1.27+ で削除)。st.rerun()です。同様に、st.experimental_memo/singleton→st.cache_data/st.cache_resource、st.experimental_fragment→st.fragment、@st.cacheも削除されました。- ほとんどの
st.rerun()呼び出しは不要です。 ウィジェットのインタラクションはすでに再実行をトリガーし、on_click/on_changeコールバックはその再実行の 前に 実行されるため、それらの状態への書き込みは別の再実行を要求しなくても参照可能です。正当な使用例:st.dialogを閉じる、それがレンダリングされた 下位 で行われたプログラム上の状態変更を適用する(ルール2のパターン)。
st.button は一瞬 — 出力が消えるバグの原因第一位
st.button() は 正確に1回の実行(クリック時)でのみ True を返し、その後は False に戻ります。したがって、if st.button("run"): st.write(result) は、ユーザーが他の何かに触れるまで結果を表示しますが、その後 消えます。そして、ネストされたボタンは両方が真になることは決してありません。内側のボタンをクリックすると外側のボタンは再び False になります(確認済み)。
# 間違い # 正しい — 状態を介して永続化する
if st.button("compute"): if st.button("compute"):
r = model() st.session_state.r = model()
st.write(r) # 次の run で消える if "r" in st.session_state:
st.write(st.session_state.r)
複数ステップのフロー: session_state 内の1つの step キーと、ルール2に従った st.rerun() を使用します。ボタンの中にボタンを決して入れないでください。
session_state のルール
- 一度だけ初期化:
st.session_state.setdefault("k", default)(またはif "k" not in st.session_state:の形式)。ウィジェットの前に、トップで行います。 - **
key="k"を持つウィジェットがこの実行で作成された 後 にst.session_state.kを書き込むと、StreamlitAPIException: cannot be modifiedが発生します。ウィジェットをプログラムで設定するには、ウィジェット行の 前に キーを書き込みます(通常: フラグを設定してst.rerun()、次の実行の先頭で書き込みを適用する)— またはコールバック内で行います。これは常に安全です。 keyがすでに session_state にある場合、value=は無視されます — 状態が優先されます。唯一の痕跡はサーバーログの警告("created with a default value but also had its value set via the Session State API")のみで、UI には何も表示されません。両方を渡してデフォルトが適用されることを期待しないでください。- ある実行でその行が実行されないウィジェットは、その状態を失います(条件による非表示タブ、折りたたまれたブランチ — 確認済み: 8に設定したスライダーが0に戻ってきました)。非表示になっても状態を維持するには、自分のキーにミラーリングします:
st.slider(..., key="_w", on_change=lambda: st.session_state.update(w=st.session_state._w))そして、書き込み-前-ウィジェットのパターンでそれをフィードバックします。(st.tabsは安全です — すべてのタブのコードは、非表示かどうかに関係なく毎回実行されます。そのため、負荷の高いタブにはキャッシュが必要です。) - 同じタイプとパラメータを持つ2つのウィジェットは
StreamlitDuplicateElementIdを発生させます — 個別のkey=を渡してください。ループ内のウィジェット:key=f"row-{i}"。
手動再実行よりコールバック
レンダリングポイントより下位で変更された状態は、1回の実行で古い値を読み取ります(確認済み: if st.button(...) ブランチは、その上のカウンターが描画された後に実行されます)。コールバックは順序を修正します — スクリプトが再実行される前に実行されます:
st.session_state.setdefault("count", 0)
def inc(): st.session_state.count += 1
st.button("+1", on_click=inc) # count はこの同じ run で新鮮
st.metric("count", st.session_state.count)
on_change は入力/選択でも同様に機能します。コールバックは任意の状態キーを書き込むことができます(ウィジェットのキーも含む、その行がまだ実行されていないため)。st.rerun() は不要です。
- コールバック内の
st.rerun()は NO-OP です(確認済み): ユーザーに見える警告Calling st.rerun() within a callback is a no-op.が表示され、コールバック本体の残りが 中断されます — 余分な再実行も例外も発生しません。クリックはすでに再実行をスケジュールしています。コールバック内ではsession_stateを変更して return するだけです。"変更を適用する"ためにst.rerun()を追加すると、コールバックが暗黙のうちに切り詰められます。
フォーム — キーストロークごとの再実行を止める
すべてのウィジェットインタラクションはスクリプトを再実行します。N個の入力がある場合、ユーザーがそれらを入力している間にN回の再実行が発生します(毎回データレイヤーにヒットします)。それらをバッチ処理します:
with st.form("params"):
a = st.slider("a", 0, 10)
b = st.selectbox("b", opts)
go = st.form_submit_button("Apply")
if go: run_query(a, b)
送信まで何も再実行されません(確認済み: 実行カウンターが合計+1)。フォーム内では: st.button なし、送信ボタン以外のウィジェットごとのコールバックもなし。
「入力中に再実行される」というのは、ほとんどの場合キーストロークが原因ではありません。 ウィジェットは異なるイベントでコミットします(Streamlit フロントエンドの動作): text_input / text_area / number_input は Enter または blur で発火、slider / select_slider は リリース時 — キーストロークごとではなく、ドラッグ中でもありません。checkbox / radio / selectbox / toggle は即座に発火します。1つのコミットされた変更 = 1回の再実行であり、値が変更されていない場合、フロントエンドは何も送信しません。したがって、入力中の継続的な再実行は、フォーカスを奪う 並行する アプリ全体の自動更新(タイマー / st_autorefresh / sleep + rerun)を意味します — その更新を、入力を含まない @st.fragment にスコープするか、入力を st.form でラップしてください。キーストロークのデバウンスは不要です。Streamlit にはそれが必要ありません。
キャッシュ — 「不良な更新」のほとんどは実際にはこれ
@st.cache_data— DataFrame / シリアル化可能な結果。呼び出しごとに新しい コピー を返します(変更は安全、isFalse を確認済み)。更新されるべきデータにはttl="15m"を。↻ ボタンにはFoo.clear()またはst.cache_data.clear()を。@st.cache_resource— 接続、モデル、スレッド。すべてのセッションに 同じ オブジェクトが返されます(確認済み)— 決してユーザーごとに変更せず、必要に応じてロックを追加してください。- 引数はハッシュ可能であり、キャッシュキーを形成する必要があります。ハッシュ不可能なものは
_をプレフィックスとして除外します(def load(_conn, day):)。 - キャッシュがない場合、すべてのウィジェットのティックがスクリプト内のすべてのロードを再実行します — 各クリックに数秒かかるため、アプリが「動かなくなったように感じます」。最初にキャッシュし、その後再実行をデバッグしてください。
正しく行うライブ更新 — st.fragment
@st.fragment(run_every="2s") # 整数秒 / timedelta / "2s" 文字列
def ticker():
st.metric("last", get_price()) # この関数のみ再実行
- ページの残りの部分は 再実行されません(確認済み: 外部の実行カウンターはフラグメントがティックしている間停止しています)。フラグメント内のウィジェットはフラグメントのみを再実行します。
st.rerun(scope="fragment")はフラグメントだけを再起動します。 - 落とし穴(確認済み): フラグメント本体がアプリ全体の実行(初回描画)の一部として実行されている場合、
scope="fragment"は エラーを発生させます — ガードするか、状態をフリップしてrun_everyに拾わせてください。 - フラグメントは session_state を自由に読み書きできます。変更をページ全体に 外に プッシュするには、状態を書き込んでから
st.rerun()します(スコープ"app"、デフォルト)。 run_everyフラグメントを外部スクリプトから停止または再タイミングする — 内部のウィジェットではできません。run_everyはアプリ全体の実行でのみ再読み取りされるため、フラグメント 内部 の間隔スライダーや停止ボタン(フラグメントのみを再実行する)はタイマーを再設定しません(確認済み)。外部からフラグメントの呼び出しをゲートしてください:完全な再実行時に、secs = st.slider("interval", 1, 10, 2) # フラグメントの外部 run = st.session_state.get("run", True) tick = st.fragment(run_every=secs if run else None)(_body) tick() if st.button("stop"): st.session_state.run = False; st.rerun()run_every=None(または呼び出さない)は自動更新メッセージを発行せず、タイマーは停止します。- フラグメントの戻り値は、完全な実行でのみ呼び出し元に到達します(確認済み)。
run_everyティックまたはフラグメント内のウィジェット変更では、外部のスクリプトは再実行されないため、data = ticker()は 古い 値を保持し、下流のチャートは決して更新されません。フラグメント 内部 で書かれたst.session_stateを介してライブデータを外部にプッシュします(st.session_state.data = fetch())。外部でそれを読み取ります — またはフラグメント自体の中でチャートを描画します。フラグメントを、ティック間で割り当て可能なプレーンな関数として扱わないでください。 - フラグメントはアプリの単一スレッドで実行されます — バックグラウンドスレッドではありません。 内部の
time.sleepはその間ページ全体を 凍結 します。run_everyを周期とし、決してスリープしないでください。ネストされたフラグメントは許可されています(確認済み)が、それぞれが独立した再実行スコープです — 子のティックは親を再実行しないため、データを所有するフラグメントにrun_everyを保持してください。同じデータに対してタイマーをネストしないでください。本当のバックグラウンド処理(遅いジョブを送信し、ナビゲーションを続け、結果が到着する) → [st-jobs]。 - 同じキーを持つウィジェットをフラグメントの内部と外部の両方で作成しないでください。
- フラグメントはティックするが、値が決して変わらない?ライブ読み取りがキャッシュされています。
ttlなしの@st.cache_dataローダーはその本体を 一度だけ 実行し(確認済み)、すべてのティックでその凍結された値を返します —run_everyは関数を再実行しますが、キャッシュがそれをショートサーキットします。これが「私のフラグメントが更新されない」原因の第1位です。修正: ライブ読み取りをキャッシュしないか、更新周期以下のttlを設定するか(@st.cache_data(ttl="2s"))、毎ティック引数を変えてください。
スコープ付きワークボックスとしてのフラグメント — 今フェッチして後で使用する(完全な再実行なし)
run_every なしのフラグメントは、そのウィジェットがボックスのみを再実行するページ内ボックスです — 「ここで参照データを検索し、後続のアクションで消費する」というパターンで、検索ごとにページ全体を再実行しません:
@st.fragment
def fetch_box(): # ここでのクリックはボックスのみ再実行
with st.form("lookup"): # フラグメント内のフォーム: 合法(確認済み)
code = st.selectbox("entity", codes, key="box_code")
go = st.form_submit_button("fetch details")
if go:
st.session_state.details = get_details(code) # STATE を介して受け渡す
st.success(f"fetched {code}")
fetch_box()
if st.button("run request"): # 外部 → そのクリックは完全な再実行であり、
d = st.session_state.get("details") # 格納された詳細を読み取る
if d is None: st.warning("fetch first")
else: run_request(d)
- コンシューマーは
st.rerun()を必要としません: それ自身のクリックは新鮮な状態を読み取る完全な実行です(確認済み)。外部ページがフェッチされたデータを(次のクリックを待たずに)即座に 表示する必要がある場合のみ、状態を書き込んだ後、フラグメント内でst.rerun()を実行します — 1回の完全な実行コストがかかります。 - 受け渡しは
session_stateを介して行い、フラグメントの戻り値は決して使用しないでください(ボックスオンリーの再実行で古くなります — 上記の箇条書き)。@st.cache_dataで検索をキャッシュし、同じエンティティの再フェッチを無料にします。
作業中のステータス表示(1回の実行内 — ループは許可)
ph = st.empty() # プレースホルダ、その場で上書き
for i, step in enumerate(steps):
ph.text(f"{i+1}/{len(steps)} {step.name}")
step.run()
ph.empty()
また、st.progress、st.status("...", expanded=True)、st.spinner、st.toast も使用できます。これらは実行内でアニメーションします — 再実行は関係なく、ループのリスクもありません。
「ループに陥った / 更新し続ける」問題のデバッグチェックリスト
st.rerunを grep — それぞれが flip-first 状態ガード(ルール2)の後ろにあるか?while True/time.sleepをスクリプトレベルで grep → フラグメントに変更(ルール1/3)。- あるウィジェットが別の行が書き戻す状態キーを書き込む → ピンポン再実行。キーごとに1つのライターを維持。
key=の衝突、または実行ごとに変化する f-string キー(例:time.time()を含む)— 毎回新しいキー = 毎回ウィジェットがリセット。- キャッシュされていない遅いロードは、通常の再実行をループのように見せかけます —
st.cache_dataと実行カウンター(st.session_state.runs += 1)を追加して、実際に何が起こっているかを確認してください。 - ライブセクションがタイマーで更新されるが、古い 値を表示する →
ttlなしの@st.cache_data関数がそれを供給しています。フラグメントは再実行されますが、キャッシュは古い値を返します。ttlを追加するか、ライブ読み取りをキャッシュしないでください。