【Streamlit検証】重い処理を含む際のキビキビした動作の実現（st.fragment）


# はじめに
Streamlit は UI 操作の度にコードの再実行が走ることを特徴としています。これによりセッション状態の管理などが大幅に軽減されるのですが、重い処理がある場合に UX の低下を招きかねません。

実際私も下図のように、重い処理を含む際のセレクトボックス操作でよく重たい印象を与える処理を記述してしまいがちです。

![blog1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3540224/ddab0b3d-6b35-dc57-f3b9-2c2a6aa7c3f4.gif)

[上図の Playground](https://streamlit.io/playground?example=blank&code=H4sIAAAAAAAAA4WQ30rDMBTG7_MUIbtpYQzc7ga7UO99gTGk7TINdm1pAm6MgWllgggOYexGNgUZMnFXCiq-zWF_fAvb1FktMnOR5PDlfOf3hTU91xdYsCZFiCUFFz41mjYT2OBRgVCsFrhNqaeVdIxzGMITCMcQvoOcQfAK4TS-h8P52WTZ7yFkGYIeuD6jHFdwB-FokW1SxlVibJE8JkZR7SVSyyfijhJNJZpKNFNxV4mWEq0iqaEuQjm8mkwheICwB8FTDCBnH_JlcT5CnuFTR-x_MbQjAi4KnNrUEqbb0hLPbHdwlXSDHIKcgLwEeQNyAPI0mqpaXE8w1-EVm3GhpQELR7TNNV1PWJlTp63KnutQpMeQ88f-35CsgTOcZWVgHTK7vpld8WeNN_F_d60jpPTVDEQtffwjSlxGceIjh5M5y-f-YnQd5Vnd3i_v3pQWoR77TFCtQdYwAwguQI4zv13GnczcLtH_scgEjix-f1Xs8Alo55NOzQIAAA)


そこで Streamlit は、キャッシュやセッション状態管理、フォーム、特定の関数のみの再実行など多様な機能を提供しています。

というわけで個人的に長年の悩みだったので、いくつかのパターンで検証してみます。


# 結論
個人的には比較的新しい機能であり特定の関数のみ再実行できる `st.fragment` が実装も簡単で、理解もしやすかったです。 
![blog-fragment.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3540224/9596bf29-8df1-1f3b-6edb-b3e60835782c.gif)

[上図の Playground](https://streamlit.io/playground?example=blank&code=H4sIAAAAAAAAA42T3UrDMBTH7_MUId60MApud4OBH_e-wBDpumwGu3U0AR1jYFqZIIJDGLuRTUGGTPRKQcW3OczpW5hmW8cCfuQiNP3n_M_5nSSs1ghCgQWrUYTYbMFFSN2azwR2uVogtMGFUwndao3WBSrTCq5Ssee5glaDsGnZeYTVWMOfozFE9xB3IHqCeAzy8Uu-fJwNtNxwQxWdRuGCcnY49aknSsGRpfckg5gu0eXMBWQf5AjkBchrkD2QJySD07CgIVhQ5wWfcWHNkzDKnQPa5JZtZ9KNrF6mR4WdoE71L1vPc4DJQ_dnAFYxGfKpqbfP_PLfbJrPTPIb30rkAnFJVzQK2l0NMFAN3BnyLPf0ufsxuFK8nzd309u3VFcYhyET1KqQRZE9iM5BDo1TyuOWUUub2P-wMZqhbFZbmbgglNxOh_uUNqycnVQN8THEQ4jfVckQvSZtVN9xf3I6mnY7CC1bpM6ipesgmySPi8RdV7eGuFk958i8Y2RLiyUtlrRYWorbWvS06GXJLmojtPoCvgGzMBtgRgMAAA)

# 検証
## 検証コード（ベース）😑
それでは最初に示した重い実装からみていきましょう。
```python
import time

import streamlit as st

time.sleep(3)  # ダミーのスリープ処理

categories = {
    "A": ["a1", "a2", "a3"],
    "B": ["b1", "b2", "b3"],
    "C": ["c1", "c2"]
}

# 親カテゴリの選択
parent_category = st.selectbox(
    "親カテゴリを選択してください", 
    options=list(categories.keys()),
    index=None
)

# 子カテゴリの選択
if parent_category:
    child_category = st.selectbox(
        "子カテゴリを選択してください",
        options=categories[parent_category],
        index=None
    )

    # 選択結果の表示
    st.write(f"選択された親カテゴリ: {parent_category}")
    st.write(f"選択された子カテゴリ: {child_category}")
```

主にこのコードには、階層になっている変数をユーザーに選択させる処理が含まれています。そして、今回の難点となる、時間のかかる処理（今回はダミーで3秒スリープ）がコードの初めに登場しています。もちろん、キャッシュやセッション状態管理によりこれを0秒に近づけることも重要ですが、どうしても時間がかかってしまうこともありえます。

そこで、今回は時間のかかる処理をスキップする方法をいくつか見ていこうと思います。

## フォームの使用😞
まず私がよく思いつくのは、`st.form` です。もちろん場合によっては良い選択なのですが今回の階層変数に対してはコレジャナイ感があります。実際に挙動を確認してみましょう。
![blog-form.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3540224/c7fbeb0e-0b4c-51d7-9a25-eb3048c38cbf.gif)

[上図の Playground](https://streamlit.io/playground?example=blank&code=H4sIAAAAAAAAA41Sy0rkQBTd11cU5aITaBpGdw1ZzLj3B6RpknT1WEweTapERQQrGVREmCCIG_EBQ-MD3ejAzKD4Mdfu1r-YSlXs1gyiWRSVe-4995x7i4W9OBFYsJAiZu5cJNQNAyawy9UPQgXY4AGlPWvGxngKQ7YO2RFktyCvIP0D2Xlxz_YHm_1RvoGQ7wr6NU4Y5djBqwirj3wmTTxP3E-kjok7rc8Z0qob8IsGPQ16GvQm4KwGfQ3606SF1hAqNOxBeqb7HisZD3cHw60cLTGxoDQ3unESWt_oilMrtay0i1DNbmrOKfzYP4f0ArINSG8K_fLqSf4ebh9quOcmNBLt51JlQlFyGlBfePGypXO0tCpLumtYQO6D7IP8AVKJ2wP5ndTHVXFPsDjiTsC4sCajaii53LLtSSKLOnTZmYsjqkM2KrUPLvO3tbNuVX5zTOgvsKDzvi1trdrkQ9Ze2ps4m68Iar0uqNg0VouztPty00_r8uH-BLIDSO8hu9YZfNELmRC0YwwVe26bWNtbFCKOrMD1aODUTHHN_v_57JS8yk-6A_JocHwzyLdAXhjPo1_58FC13H08OR39_IvUjMdNzXRV36WECWp1yfOYSqrKE2ni1co01oj9DkVlFYri9SILhn8TSyodyAMAAA)

ソースコードは下記です。
```python
import time
import streamlit as st

time.sleep(3)  # ダミーのスリープ処理

categories = {
    "A": ["a1", "a2", "a3"],
    "B": ["b1", "b2", "b3"],
    "C": ["c1", "c2"]
}

# `st.form` の使用
with st.form(key='category_form'):
    # 親カテゴリの選択
    parent_category = st.selectbox(
        "親カテゴリを選択してください",
        options=list(categories.keys()),
        index=None
    )

    # 子カテゴリの選択
    if parent_category:
        child_category = st.selectbox(
            "子カテゴリを選択してください",
            options=categories[parent_category],
            index=None
        )
    
    # フォーム送信ボタン
    submitted = st.form_submit_button(label='送信')

# フォームが送信された場合に選択結果を表示
if submitted:
    st.write(f"選択された親カテゴリ: {parent_category}")
    st.write(f"選択された子カテゴリ: {child_category}")
```

`with st.form` 内の処理が、フォームとして一部だけ実行できるのですが、この場合 `st.form_submit_button` が必須のコンポーネントとなります。これにより、ボタンを押した際しか `parent_category` の値が更新されないため、`child_category` に表示されている値にズレが生じてしまっています。

`st.form_submit_button` を押下した際に重い処理も実行されてしまうため、いずれにしても期待した動作にはなっていません。全く用途が違う訳ですね。


## `st.fragment` の使用😊
既にネタバレ済みですが、再度 `st.fragment` を使用した挙動から確認してみましょう。内容は序盤に提示したものと全く同じです。
![blog-fragment.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3540224/9596bf29-8df1-1f3b-6edb-b3e60835782c.gif)

[上図の Playground](https://streamlit.io/playground?example=blank&code=H4sIAAAAAAAAA42T3UrDMBTH7_MUId60MApud4OBH_e-wBDpumwGu3U0AR1jYFqZIIJDGLuRTUGGTPRKQcW3OczpW5hmW8cCfuQiNP3n_M_5nSSs1ghCgQWrUYTYbMFFSN2azwR2uVogtMGFUwndao3WBSrTCq5Ssee5glaDsGnZeYTVWMOfozFE9xB3IHqCeAzy8Uu-fJwNtNxwQxWdRuGCcnY49aknSsGRpfckg5gu0eXMBWQf5AjkBchrkD2QJySD07CgIVhQ5wWfcWHNkzDKnQPa5JZtZ9KNrF6mR4WdoE71L1vPc4DJQ_dnAFYxGfKpqbfP_PLfbJrPTPIb30rkAnFJVzQK2l0NMFAN3BnyLPf0ufsxuFK8nzd309u3VFcYhyET1KqQRZE9iM5BDo1TyuOWUUub2P-wMZqhbFZbmbgglNxOh_uUNqycnVQN8THEQ4jfVckQvSZtVN9xf3I6mnY7CC1bpM6ipesgmySPi8RdV7eGuFk958i8Y2RLiyUtlrRYWorbWvS06GXJLmojtPoCvgGzMBtgRgMAAA)

<details><summary>💡ちなみに：st.fragment とは？</summary>

`st.fragment` は `Streamlit` でアプリ全体ではなく特定の一部（フラグメント）のみを再実行できる機能です。ウィジェットがフラグメント内で操作された場合、アプリ全体の再実行ではなくそのフラグメントのみが再実行されるため、効率的な処理が可能になります。

例えば、複数の可視化やフォームがあり、個別に更新したい場合に便利な機能です。そういう訳で、今回のユースケースにぴったりという訳ですね。
</details>


ソースコードは下記です。
```python
import time

import streamlit as st

@st.fragment
def get_category():
    # 親カテゴリの選択
    parent_category = st.selectbox(
        "親カテゴリを選択してください", 
        options=list(categories.keys()),
        index=None
    )
    
    # 子カテゴリの選択
    if parent_category:
        child_category = st.selectbox(
            "子カテゴリを選択してください",
            options=categories[parent_category],
            index=None
        )
    
        # 選択結果の表示
        st.write(f"選択された親カテゴリ: {parent_category}")
        st.write(f"選択された子カテゴリ: {child_category}")


time.sleep(3)  # ダミーのスリープ処理

categories = {
    "A": ["a1", "a2", "a3"],
    "B": ["b1", "b2", "b3"],
    "C": ["c1", "c2"]
}

get_category()
```

実装において特に難しいポイントはなく、セレクトボックスによる処理を関数にし、その関数に `st.fragment` デコレータを付与するだけです。これにより、この関数の中の操作だけが再実行されるため、キビキビした `child_category` への情報更新が行われます。

結構信じられないくらい快適になるので、ぜひ覚えておきたいテクニックですね。（というか僕が覚えておきたいから記事にした）


## st.fragment の副作用・考慮しておくべきこと
`st.fragment` は上記のように大変便利な一方、多用することでセッション状態管理に複雑性をもたらす可能性もはらんでいるはずです。

例えば、先の `st.fragment` 外で先程の値を取得するにはどうしたら良いでしょうか？それは、全体の再実行、あるいは値の取得部分の一部実行をすることで解決します。実際の挙動を確認してみましょう。
![blog2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3540224/ba94b7dc-6821-8b0f-3859-b751ff589597.gif)

[上図の Playground](https://streamlit.io/playground?example=blank&code=H4sIAAAAAAAAA6VTT2sUMRy951OEeMnAsmB7G1jwz92Tt1IkM_vb3eBssiQZ21IKzoxUEMFFKL1IW0EWqehJQcVvE9qt38JMZjrLBrRC5xAmeXkvv_d-CZ_OpDLY8CkgxJuJNgrYNOMGM-0mCN3Tpj9SbDwFYWiEhjDCYzBPUmZgLNUejWKE3XcHXy3ObfnJVoe2_Gqrc1t8-V18v3x14uEZU47fsfDAafc1ZJCaRO5Sv6f-SKhSvm1UbHFsi4Ut3tjizBZHtnhBerijyZnhUuhBxrWh7SEcdP8p7GkaRb1uIxdD2B08kgL8UuTH1sDF5_nfDfBR6CHuRNMJz4Y3e_P-wkP-5W-NeW1x5W4rKGh7nRBYDew2lpuzl9_mlyfvnN-r9x-XH352uLOxo7gBOiLXRR7Z8rUtToMuxXg_qOWARP8hE4ThZNajrFU6GQUmVyLsQi9Iv9kPmYY4ZNZR9PyIEKovfV9nADO6GdVR2Oq5rU5t9cvlYMsfdW_cf3V88XKxnB8itMrdNXjfa5P7JMZbhN11V5GwDT9ukrYN5IEHEw8mHkxW4EMPph5MN8g2OnA17XAzqdNKpTCMC1A0kWoIavBY5dC-sxvsu9rWXydC9WXMkwkwp0RJm8UzluXg0r1li2_ZWkdPcmOkqOtSuXBrfwDIemLJlAQAAA)

このように `st.fragment` 内外で状態にズレが生じるため、この特性を意識した上で活用すると良さそうです！

# まとめ
今回は階層構造になっている変数をキビキビと選択するための方法を検討してみましたが、他にも色々と Streamlit の API のどれを選ぶかによって UX に変化がありそうです。

今後もこうした悩みに遭遇する度に紹介していければと思っていますし、こういうときどうしよう？などディスカッションしましょう。

また、こういう実装でもキビキビ動くぜ！みたいなものがありましたら、ぜひコメント・リプライなどでリアクションいただけると嬉しいです。大事な API を見逃している可能性も大いにあるので・・・。