Streamlitについて

お疲れ様です。

今日は「Streamlitについて」について部分いたします。

 Streamlitについての簡単な説明と、Streamlit を使う上で私がハマったポイントの紹介をしておきます。
 
# Streamlit とは

Streamlit turns data scripts into shareable web apps in minutes. All in pure Python. No front‑end experience required.

公式の謳い文句をざっくり訳すと以下の通りです。

- データスクリプトを数分で共有可能な Web アプリにできる
- 全て Python で書けてフロントエンドの知識は必要ない

データスクリプトというのはおそらく`str`や`pandas.DataFrame`などでしょうか。データ分析ではお馴染みの`DataFrame`を雑に渡すだけでテーブル表示をしてくれます。HTML/JS/CSS を書く必要はなく、事前に用意されたコンポーネントを組み合わせてページを作成します。公式のGalleryを見るだけでも、`Streamlit`の魅力が実感できるでしょう。

printを同じような書き方で Web 画面が出来るので、アプリエンジニアだけでなくデータサイエンティストなども簡単にデモアプリを作成できます。むしろ「機械学習やデータサイエンスのデモアプリを短時間で用意する」ことを主なユースケースとして想定しているようです。

## 基本的な機能

以下の手順で簡単に試すことができます。

### streamlit をインストール

```terminal
pip install streamlit
```

### サンプルコードを用意する

```python
# main.py
import streamlit as st


def main():
    st.write("Hello World!")


if __name__ == "__main__":
    main()

```

### 実行する

```terminal
streamlit run main.py
```

デフォルトでは`http://localhost:8501`で起動するため、ブラウザ等で簡単に作成した Web ページにアクセスすることができます。その他にも様々なコンポーネントが用意されています。公式ドキュメントやもみじあめさんの記事で紹介されていますので、適宜参照して下さい。

https://docs.streamlit.io/library/api-reference
https://blog.amedama.jp/entry/streamlit-tutorial

以降の節では ここで紹介していない様々なウィジェットや 、Streamlit を使う上で私がハマった「状態管理」「複数ページ対応」の 2 点について紹介していきます。


# 状態管理

Streamlit でセッション管理をするには、st.session_stateを使います。st.session_stateは Python の辞書型オブジェクトのように扱うことができ、ユーザアクションに応じて変化する状態を保持することが出来ます。

[公式ドキュメント](https://docs.streamlit.io/library/advanced-features/session-state#example-1-add-session-state)で挙げられている、カウンターの例を参考に説明していきます。カウンターの完成イメージは以下の通りです。

![1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3507088/77a03a62-d90a-ce1e-cd9c-a5228caf8675.gif)

実装は以下の通りです。

```python
import streamlit as st

st.title("Counter Example")
if "count" not in st.session_state: # (C)
    st.session_state.count = 0 # (A)

increment = st.button("Increment") # (B)
if increment:
    st.session_state.count += 1 # (A)

st.write("Count = ", st.session_state.count) # (A)
```

コード中(A)の部分で`st.session_state`に`count`変数の書込、読込を行います。

コード中(B)の`increment`は,
ボタンが押下されたタイミングで True になります。

ではコード中(C)は何の意味があるのでしょうか？
(C)を除いて実行すると、以下のようにカウントが正しくされなくなります。

![2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3507088/f82d9aad-f6a4-243e-d0da-399e4ba187c4.gif)

この原因を知るには Streamlit の描画のタイミングを理解する必要があります。

## 描画のタイミング

描画のタイミングの英語

`
Every time a user interacts with a widget, your script is re-executed and the output value of that widget is set to the new value during that run.
`

ざっくり訳すと「ユーザがウィジェット(ボタンなど)を操作するたびにスクリプトが再実行され、そのウィジェットの出力値が新しい値になる」です。つまり(C)を削除すると、ボタンが押されるたびに`st.session_state.count = 0`が実行されます。`count`が 0 になった後で 1 が加算されるため、何度ボタンを押しても 1 のままということになります。

そのため`st.session_state`はアプリが初期化されているかの状態を保持することがよくあります。

## コールバック処理

`st.button`など、いくつかのウィジェット では「クリックされた時の処理を記述する」on_clickや「値が変化した時の処理を記述する」on_changeを引数として、コールバック関数を渡すことが可能です。カウンターをコールバック関数を用いて書くと以下のようになります。

```python
import streamlit as st

st.title("Counter Example using Callbacks")
if "count" not in st.session_state:
    st.session_state.count = 0

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

st.button("Increment", on_click=increment_counter) # 戻り値は押下された時にTrueとなる

st.write("Count = ", st.session_state.count)
```

では戻り値で条件分岐するパターンと、コールバックを用いるパターンはどのようにかき分けたら良いのでしょうか。

## コールバックを使うケース

`st.text_input`のような入力を受け付ける「ウィジェットの値」を変更したい場合にコールバックを使う必要があります。以下のようにボタンを押すと入力ウィジェットの値が変わる例を考えてみます。

![3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3507088/cb2c2706-540a-ca09-eba2-bca6e9c2781c.gif)

実装は以下のようになります。

```python
import streamlit as st

st.text_input("Message", key="text_input") # (A)


def change_value():
    st.session_state["text_input"] = "Hello, World" # (B)


st.button("Click", on_click=change_value) # (C)
```

コード中の(A)についてですが、`key`を渡されたウィジェットは、自動的に`st.session_state`にそのkeyで追加されます。そして(B)のようにウィジェットの値(`value`)を変更することが出来ます。最後に(C)のようにコールバック関数を渡すことで、入力を受け付けるウィジェットの値を変更することが可能です。

「コールバックを使わずにifで書くとどうなるの？」と思った方もいるかもしれませんが、例外(`StreamlitAPIException`)が発生します。

```python
# ※ボタン押下時に例外が発生する
import streamlit as st

st.text_input("Message", key="text_input")


if st.button("Click"):
    st.session_state["text_input"] = "Hello, World"

```

また、`key`を使えば以下のように初期値を定めることも可能です。ただし、`st.session_state`とウィジェットのvalueの両方を指定すると `warning` が発生するので注意して下さい。


```python
import streamlit as st

key = "text_input"

def change_value():
    st.session_state[key] = "World"

if key not in st.session_state:
    st.session_state[key] = "Hello"

st.text_input("Message", key=key)
st.button("Click", on_click=change_value)
```

`st.session_state`やコールバックについては公式ドキュメントに記載があるので適宜参照して下さい。

https://docs.streamlit.io/library/api-reference/session-state#caveats-and-limitations

# 複数ページ対応

次に複数ページ対応について説明します。Streamlit に「複数ページ対応」や「ページルーティング」といった機能は(執筆時点では)ありません。一方でユーザアクションに応じて実行する関数を切り替えることで「複数ページ」あるように見せることが可能です。

以下のようにボタンを押すとページが切り替わる例をみてみます。

![4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3507088/5b104f98-a026-8082-d40c-48a12f25cbb1.gif)

この実装は以下のようになります。

```python
import streamlit as st

def page1():
    st.title("Page1")

def page2():
    st.title("Page2")


pages = dict(
    page1="ページ1",
    page2="ページ2",
)

page_id = st.sidebar.selectbox( # st.sidebar.*でサイドバーに表示する
    "ページ名",
    ["page1", "page2"],
    format_func=lambda page_id: pages[page_id], # 描画する項目を日本語に変換
)

if page_id == "page1":
    page1()

if page_id == "page2":
    page2()
```
selectboxの値に応じて、実行する関数を切り替えることで複数ページのように見せています。

## ページ間でデータをやり取りする

実際の Web アプリではページがただ切り替わるだけではなく、ユーザが入力した値が別ページで表示されることもあります。以下のような例で見てみましょう。

![5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3507088/ced0e1c7-e5c9-85f1-ac08-0f948f93b5bd.gif)

これを Streamlit で行うならば、既に紹介した`st.session_state`を用います。page1 と page2 の関数を以下のように修正すれば OK です。

```python
def page1():
    st.title("What's your name?")
    st.text_input("Name", key="name")


def page2():
    name = st.session_state["name"]
    st.title(f"Hello, {name}")
```

既に紹介した`st.text_input`にkeyを指定することで自動的に`st.session_state`に値を入れるパターンですね。

## 遷移するボタンを作る

データのやり取りは出来ましたが、実際の Web アプリはこうではないと思います。以下のように名前を入力し、確定ボタンを押したら遷移する方がしっくり来ないでしょうか？

![6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3507088/4f9fea0f-3f48-0993-e81e-922d93da8f3b.gif)

これは page_id をセッション管理することで実現可能です。まずselectboxにkeyを持たせてセッションで保持します。


```python
page_id = st.sidebar.selectbox(
    "ページ名",
    ["page1", "page2"],
    format_func=lambda page_id: pages[page_id],
    key="page-select",
)
```

次に page1 のボタン押下時に、selectboxの値を page2 に変更することでページが遷移したように見せます。なお、これは入力を受け付けるウィジェットの値を変更することになります。そのためon_clickでコールバック関数を使う必要があるので注意して下さい。


```python
def page1():
    st.title("What's your name?")

    def change_page():
        st.session_state["page-select"] = "page2"

    with st.form(key="name-form"):
        st.text_input("Name", key="name")
        st.form_submit_button(label="Submit", on_click=change_page)
```

今日は以上です。

次からは「Pythonだけで作る Web アプリケーション(フロントエンド編)」について紹介いたします。


ありがとうございました。
よろしくお願いいたします。