/home/by-natures/dev*

データ界隈で働くエンジニアとしての技術的なメモと、たまに普通の日記。

2023/09/01 Streamlit 入力ウィジェットでの変数の扱い方

簡単に Web アプリケーションが作れて人気の Streamlit ですが、入力ウィジェットも豊富に揃っていて便利です。

一方であまり学ばずともそれっぽいモノが作れてしまうため、変数周りの扱いでつまづくことが多いのではないでしょうか(私のことです。。)。備忘録も兼ねて、誤った設定と正しい設定方法を見ていきます。

もくじ:

TL;DR

入力と出力はセッションステート変数を分ける、もしくは key 属性を極力活用すると読みやすくウィジェット間のやり取りが分かりやすいコードになります。

検証

OK: 初期引数を固定し、セッションステートで結果を受け取る

st.session_state["st_title"] = st.text_input(value="Titanic", label='Movie Title')
st.write(f"session state 'st_title: {st.session_state['st_title']}")

もしくは key を使って:

st.text_input(value="Titanic", label='Movie Title', key='st_title')
st.write(f"session state 'st_title: {st.session_state['st_title']}")

好きな映画タイトルを入れたときに、セッションステート変数 st_title がどう変わるかを見てみます:

予想通り、Titanic -> Avatar -> Green Mile と入力したものが、そのままセッションステート変数に反映されていることが分かります。

NG: 入力と出力に同じセッションステート変数を利用(key 未使用)

他のウィジェットの結果を用いたりするなど、初期値自体にセッションステートを与えたい場合もあるかもしれません。同じセッションステート変数を使い回してみます:

if 'st_title' not in st.session_state:
    st.session_state["st_title"] = "Titanic"

st.session_state["st_title"] = st.text_input(value=st.session_state["st_title"], label='Movie Title')
st.write(f"session state 'st_title: {st.session_state['st_title']}")

これは予想を裏切り、挙動が不安定になります。映画タイトルを入れて Enter キーを押すと、入力が前の値に戻ってしまったり、消えてしまっています。

OK?: 入力と出力に同じセッションステート変数を利用(key 使用)

同じセッションステート変数でもウィジェットの key 属性を通じて設定すると、うまく動きました。

if 'st_title' not in st.session_state:
    st.session_state["st_title"] = "Titanic"

st.text_input(value=st.session_state["st_title"], label='Movie Title', key='st_title')
st.write(f"session state 'st_title: {st.session_state['st_title']}")

この短いコードでは正しく動きましたが、アプリケーションを複雑にしていったときにもお勧めできるかは分からないため、「OK?」としました。該当のセッションステート変数の更新箇所が複数に渡るためコードも読みづらくなると思いますし、明確な意図がなければ避けた方が良さそうです。

OK: 入力と出力に違うセッションステート変数を利用

初期値と出力にどちらもセッションステートを使いたい場合は、変数名を変えれば正しく動きます。

if 'st_title_init' not in st.session_state:
    st.session_state["st_title_init"] = "Titanic"

st.text_input(value=st.session_state.st_title_init,
              label='Movie Title',
              key='st_title_edited')

st.write(f"session state 'st_title_edited: {st.session_state['st_title_edited']}")

複数個組み合わせて数珠繋ぎにもできます:

import streamlit as st

if 'st_title_init' not in st.session_state:
    st.session_state["st_title_init"] = "Titanic"

st.text_input(value=st.session_state["st_title_init"], label='Movie Title1', key='st_title_edited1')
st.text_input(value=st.session_state["st_title_edited1"], label='Movie Title2', key='st_title_edited2')
st.text_input(value=st.session_state["st_title_edited2"], label='Movie Title3', key='st_title_edited3')

st.write(f"session state 'st_title_init: {st.session_state['st_title_init']}")
st.write(f"session state 'st_title_edited1: {st.session_state['st_title_edited1']}")
st.write(f"session state 'st_title_edited1: {st.session_state['st_title_edited2']}")
st.write(f"session state 'st_title_edited1: {st.session_state['st_title_edited3']}")

こんな直接的にウィジェットを繋げることはないと思いますが、例えば「入力された日付を別のテキストエリアに反映させたい」など、異なるウィジェットを組み合わせて少し複雑なアプリケーションを作ろうと思ったときに、結果的にウィジェットがセッションステートを通じて繋がることはあるかと思います。

セッションステート変数の数は増えてしまいますが、入力と出力に同じ変数を利用しないことで、その変数がどのウィジェットで更新されたものか分かるようにもなるためデバッグもしやすくなると思います。

まとめ

入力と出力はセッションステート変数を分ける、もしくは key 属性を極力活用すると読みやすくウィジェット間のやり取りが分かりやすいコードになります。

以下のコールバックのことを考えると、基本的には key 属性を利用する方法がよさそうです。

(参考)コールバックで新しい値を参照する方法

コールバック関数内で新しい値を参照するには、「key を通じて設定されたセッションステートにコールバック内でアクセスする」ことで唯一成功しました。なお args, kwargs を使っても、古い値が渡ってしまうため使えません。

公式ドキュメントでも key を通じてコールバックから新しい値を参照しているため、これが正しい方法のようです。

if 'st_title_init' not in st.session_state:
    st.session_state["st_title_init"] = "Titanic"

def callback():
    st.write(f"Callback: session state 'st_title_edited: {st.session_state['st_title_edited']}")

st.text_input(value=st.session_state.st_title_init,
              on_change=callback,
              label='Movie Title',
              key='st_title_edited')