Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Streamlitの細かい話

 Streamlitの細かい話

NISHIKAWA, Daisuke

March 16, 2025
Tweet

Other Decks in Technology

Transcript

  1. AI 3 ▪ 西川 大亮 ▪ 2019/4 DeNA入社、2020/4 GO転籍(当時Mobility Technologies)

    ▪ 前職: 中堅SIer ▪ 14年研究所、3年コンサル ▪ 顧客向け技術紹介と火消しが主な仕事 (Projが燃える匂いには敏感) ▪ 趣味 ▪ ゴルフ(年数回) ▪ 競馬予想(サボり気味) ▪ 設計欲を満たせるゲームにハマりやすい ▪ やっていること ▪ タクシーアプリ『GO』(主に乗務員端末)の挙動解析 ▪ 課題解決:原因調査→類型化→自動検知 ▪ 性能改善:現状把握→KPI定義→レポート+指針策定 ▪ システムよりも人間系の解析が中心 自己紹介
  2. AI 4 背景(Streamlitとは) • https://streamlit.io/ ◦ Streamlit turns data scripts

    into shareable web apps in minutes. ◦ All in pure Python. No front‑end experience required. • 国内だと“PythonのWebアプリフレームワーク” と表現されることが多い • GO Inc. では主にBiz向けの情報共有ツールとして 活躍している ◦ “グラフや統計値では意味が取れないが、スプ シだと量が多くて辛い” ぐらいの粒度で使う • 簡単さを重視する分、お約束が多くて癖がある • 今回はその細かい癖のお話 調査時のStreamlitのバージョン: 1.41.1 地図に位置を表示するだ けならこのぐらいで書け る データ: bigquery-public-data.chicago_ta xi_trips.taxi_trips 注 Speaker Deckだとアニ メーションGIFが動きません ごめんなさい…
  3. AI • streamlit run コマンドの引数が開始地点 ◦ streamlit run app.py ならapp.pyから実行される

    • なので、vscodeなどでのデバッグ実行も同じ(右) ◦ invokeコマンドをデバッグするのと同じ形 6 どこから始まるのか?(起動の細かい話①) • ユーザーコードの開始地点の話 • 自分で環境を作っていると自明だが、既に環境がある とdockerやkubanetesのファイルに紛れてわからなく なることも // streamlit 挙動確認用 { "name": "streamlit", "type": "debugpy", "request": "launch", "justMyCode": false, "console": "integratedTerminal", "cwd": "${workspaceFolder}", "env": { "PYTHONPATH": "${workspaceFolder}", }, "module": "streamlit", "args": [ "run", "${workspaceFolder}/apps.py", ] } • (おまけ)実行は別スレッド ◦ スレッドなので複数のブラウザから同時にリクエス トしても処理するのは1つずつになる ◦ 並列処理はWSGI(uwsgi, gunicorn, etc.)を使うこと になりそう
  4. AI 7 いつ始まるのか?(起動の細かい話②) • ユーザーコードの起動タイミングの話 • コマンドラインで指定しているのだからそのタイミン グ…ではない。Webアプリなので • ブラウザ等からリクエストがあったタイミングで呼び

    出される ◦ JavaScriptが実行できないとダメ ▪ 単にcurlだと届かない ▪ デフォルトページはStreamlit側で出している ◦ もしE2Eテストしたいなら、Seleniumなどが必要 ◦ 内部はスレッドなので、処理が複数あると前が終わ るまで待たされる
  5. AI 8 UI操作時はどこから始まるのか?(起動の細かい話③) • ブラウザでURLを叩くと、コマンドで渡したファイルが起動する なら、画面のボタンを押した時は? • 通常はコマンドで渡したファイルが先頭から呼ばれる ◦ ボタンのコードからではない

    ◦ 右上のように書くと、1行目から開始し、if文を通って最初は6 行目が、ボタンを押すと4行目が実行される。 • 例外その1はイベントハンドラ(on_*)を定義した場合 ◦ イベントハンドラ→渡したファイルがまた先頭から呼ばれる • 例外その2はfragmentを定義した場合 ◦ @st.fragment内の関数だけ呼ばれる ▪ 関数の外側は呼ばれない 1回目の実行で先頭からpush this!を通り、 2回目の実行で先頭からpushed!を通る。2回目 が4行目から始まるのなら、pushed!はpush this!のあとに出るはず。 イベントハンドラ内 で描画要素を作ると 先に描画される(ア ンチパターン)
  6. AI 10 実行順に描画される(描画の制御①) • 画面構造を定義する機能はない ◦ htmlのようなツリー構造で表現できない • 描画命令を呼んだ(実行)順で上から下に表示される ◦

    先に出した描画要素を後のUIのデータで更新できない(右) ◦ シンプルに書くなら上のUIで絞って下のwidgetに表示さ せる画面構成になる • 表示位置の自由度を上げるには以下の方法がある ◦ on_* で状態を更新する ▪ 簡略版としてst.(*, key=”session_key”)でセッション 値を直接更新させる ◦ st.* の戻り値を使う ◦ st.empty/st.container で遅延生成する ◦ st.rerun() で再描画する ボタンで数値をカウン トアップ ボタンを押した結果が 反映されるのは下のラ ベルだけで、(このま までは)上のラベルに 反映できない session_stateについ ては後述
  7. AI 11 表示位置の自由度を上げる(描画の制御②) • on_* で状態を更新する ◦ 描画と処理を分けられる(◯) • st.empty/st.container

    で遅延生成する ◦ 描画と処理が混ざる(△) • st.* の戻り値を使う ◦ 描画が2ヶ所になる(✕) • st.rerun() で再描画する ◦ 処理が冗長/複雑になる(✕✕) どれも結果は同じ 再描画時には先程のボタン クリックのメッセージは消 費されていてif文がFalseに なるのもややこしい
  8. AI 12 表示位置の自由度を上げる(描画の制御③) • st.(*, key=”session_key”)でセッション値を直接更新 させる ◦ on_changeの関数に変更値を渡せないので、スライ ダなど値を返すUIに対してはこの方法しかなさそう

    ◦ 更新処理を書かなくて良い(◎) このやり方、マルチページ の切り替えでセッションが 消える問題がある(後述) st.sliderの戻り値が今の値になるが、 これを使うとハマる
  9. AI 13 stopとrerunとfragment(描画の制御④) • st.stop() ◦ その場ですべての処理を終了する ▪ 以降の描画要求を捨てる設定を入れ例外で脱出 •

    st.rerun() ◦ その場で処理を停止し、また最初から実行する ▪ ある条件でrerun→別の条件が揃いrerun→…があ るので注意 • st.fragment ◦ 関数にデコレーター@st.fragmentをつけることで、 関数内からの処理で描画したUIを操作すると、この 関数が呼び出される。 ▪ イベントハンドラは関数の前に呼ばれる ▪ streamlit run で指定したファイルは呼ばれない! stopとrerunはgoto的な大域脱出なので、コードの複雑さを 招きやすく使わない方が良い。 fragment()内のボタンを押すとこの関数だけ実行される fragment()外のボタンを押してもfragment()は実行される UIのイベントを流すキューが変わる 関数しか実行されないのでレイアウトをいじる目的では使えない 画面が複雑化した時に処理を減らし認知負荷を下げる効果がある
  10. AI 14 制御機構の細かい癖から気をつけること • 実行順にUIが表示される ◦ シンプルに書きたいなら、画面上部で処理条件、画 面下部で処理結果を出すデザインにする ◦ そうも言ってられない時は、イベントハンドラで処

    理と描画を分ける • UIのどこを触っても全体が実行される ◦ 途中の状態でも処理が流れるので対応が必要 ◦ 処理を軽くする必要がある ▪ セッションやキャッシュ(後述)で重い処理 (CPU/IO)の結果を保持する ▪ fragmentで更新範囲を制限する(副次的)
  11. AI 16 キャッシュ[cache_data, cache_resource](保存機構①) • 関数にデコレーターst.cache_data をつける ◦ 関数への引数をkeyとして、関数の戻り値をpickleで 保持し、複製を返す

    ◦ 普通にデータを読み込んだかのように使える ▪ 通常はこちらを使う • st.cache_resourceというデコレーターもある ◦ 関数への引数をkeyとして、関数の戻り値をそのまま 保持し、実体を返す ◦ 複製不可のオブジェクトに使う ▪ コネクションが典型 • keyはハッシュ化できれば何でもいいので、DBからデー タを取るならSQLクエリをそのまま渡すと楽ができる • Pythonインスタンスで1つなのでWSGIを使うとインス タンスの数だけメモリを使う(△) 一度ロードするとキャッシュが残り、リ ロードしてもアクセスが遅くならない
  12. AI 17 セッション[session_state](保存機構②) • ブラウザタブ単位のdict ◦ BrowserWebSocketHandler.open でオブジェクトがで き、BrowserWebSocketHandler.on_closeでオブジェクト を消しているので、WebSocketのコネクション単位

    • widgetにkey=”session key”を指定すると、 st.session_state[“session key”]に値が入る ◦ 値はon_*イベントハンドラの前に入る ▪ イベントハンドラへ引数を渡せないため、UIの値を渡す 唯一の方法(のはず) ▪ このルールを知らない人には理解できないのが欠点 ◦ ただし、st.navigation でページを切り替えるとkeyが消さ れる⋯これはいただけない ▪ (st.navigationに限らず)widgetがレンダリングされ ないと消される仕様のため ▪ 回避方法は後述 st.sliderでkeyに指定した値はpageを跨ぐことで消える なお、デフォルト値を書き換えるとwidgetが新規に作 られるので、掴んでドラッグができなくなる…
  13. AI 18 URL[query_params](保存機構③) • クエリパラメタ ◦ 書き出すとブラウザのアドレスに反映される ◦ urlを貼り付けて共有できるようになるので、クエリパ ラメタを主、セッションを従で実装できると良さそう

    ▪ 権限管理上もスクショより良い • クエリ↔セッション 変換の支援機能はなさそう ◦ 処理条件が増えるとベタ書きは辛い • こいつもst.navigationのページ遷移で消える ◦ st.Pageでクエリパラメタが渡せない ◦ ページ間で情報を渡したいならセッションに入れる
  14. AI 20 複数ページで状態を保持する(応用①) • widgetのkey指定だとページ切り替えで状態 が消える ◦ イベントハンドラで別名に退避させる ▪ ページIDを採番して保持

    ◦ ページIDと実体が変わったら復帰 page2はurlパラメタも同期させた ここまで定型だとクラス化したくなる なおurlに直に打ち込むとセッションが初期化される 回避にはlocalStorageが良さそうだけどStreamlitに口がない
  15. AI 21 階層セレクター(応用②) • 施設の緯度経度データについて ◦ エリア(区など)を選択したら所在 する施設だけにしたい ◦ エリアも階層化したい(都道府県

    →市区町村) ◦ 未選択なら全部出したい • 実装方式 ◦ selectboxの設定値はon_change で更新してセッションに入れる ◦ 表示データはセッションに入っ たセレクタの値で絞る ◦ UIの設定値はセッションの値を 使う。selectboxの戻り値で何か やろうとしない データ: https://nlftp.mlit.go.jp/