streamlitのマルチページアプリでsession_stateをページごとに管理する方法

# 概要
　StreamlitでマルチページのWebアプリを作る際、`session_state`はページをまたいで共有されます。そのため、たとえば`page1.py`で`text`というキーを使用している場合、`page2.py`の`text`も連動してしまいます。そこで、ページごとに`session_state`を管理できる[pgss](https://github.com/masashi2ueda/pgss)というパッケージを作成しました。

# 背景
　あなたは上司からある日「StreamlitでChatGPTを組み込んだ複数ページのアプリ作って」と言われたことはあるだろうか？わたしはある。そして、「Streamlitってなんだっけ？」と心の中で思いつつも、「仕事ができる人間だと思われたい」という悲しき性（さが）から、二つ返事で「了解しました！」と答えてしまった。
 　しかし、さすが流行りのフレームワークだけあり、[チュートリアル](https://docs.streamlit.io/develop/tutorials)を試しているうちに、「なんでも作れそう」という気分になる。実際2～3個のアプリページを作成し、「なんだおれは天才だったのか」と思った矢先、確認作業中に気づいた。「あれ、ページAのメッセージ履歴がページBにも表示されてる...`session_state`がページ間で共有されてないか？」

# 問題の確認
以下のような構成のマルチページWebアプリを作成したとする。
```css
app
├── main.py
├── page1.py
└── page2.py
```

`main.py`の内容
```python
import streamlit as st
pg = st.navigation([st.Page("page1.py"), st.Page("page2.py")])
pg.run()
```

`page1.py`の内容
```python
import streamlit as st
if 'count' not in st.session_state: 
	st.session_state.count = 0
if st.button("Increment"):
    st.session_state.count += 1
st.write("This is page1")
st.write(f"count: ", st.session_state.count)
```

`page2.py`の内容(`page1.py`と同じ)
```python
import streamlit as st
if 'count' not in st.session_state: 
	st.session_state.count = 0
if st.button("Increment"):
    st.session_state.count += 1
st.write("This is page2")
st.write(f"count: ", st.session_state.count)
```

各ページで`count`を`session_state`に保持し、ボタンが押されるたびにインクリメントして表示するシンプルな例である。実際にこれを動かすと、まず`page1`が開く。ボタンを数回押した後の画面は以下の通り。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138090/81b14cff-57ae-d8e7-02fc-4d06cda281df.png)
次に`page2`に移動すると、本来`count`が0であるべきだが、以下のように`count`が3のままになっている。
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138090/7319521a-94df-e91d-8263-f5f61dad23f0.png)
 このように、`session_state`はページ間で共有されているため、複数ページのチャットアプリなどを作成すると、会話履歴がすべてのページで共有されてしまう問題が発生する。

# 解決方法①：ページ名を`session_state`に含める
　最初に思い付く解決方法は、`session_state`のキーにページ固有の名前を追加することだ。たとえば、以下のように`page1`と`page2`を修正すると、`count`の値がページごとに独立するようになる。

`page1.py`の内容（ファイル部分以外`page2.py`も同様）
```python
import streamlit as st
page_name = "page1.py"
if f"{page_name}count" not in st.session_state: 
	st.session_state[f"{page_name}count"] = 0
if st.button("Increment"):
    st.session_state[f"{page_name}count"] += 1
st.write("This is page1")
st.write(f"count: ", st.session_state[f"{page_name}count"])
```

`page1`で数回ボタンを押した状態：
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138090/880bd0bf-322d-0871-287d-c35121a07b04.png)
 
`page2`に移動した状態。`count`は独立しており、0になっている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/138090/c50492b8-fbdf-4eee-39aa-ada842b52c09.png)

これで問題は解決したかに見えるが、すべてのコードにページ名を追加する作業は非常に手間がかかる。そこで、趣味でより簡単な解決策を考えて、以下のようなパッケージを作成した。


# 解決方法②：`session_state`のラッパーを用意する
　発想はシンプルで、解決方法①の`session_state`にページ名を追加する部分をラッパーで隠蔽しただけである。このアイデアを基に、[pgss](https://github.com/masashi2ueda/pgss)というパッケージを作成した。これを使用すると、以下のように記述できる。

`page1.py`（ファイル名部分以外`page2.py`も同様）：
 ```python
import streamlit as st
from pgss import PageSessionState
ps = PageSessionState("page1.py")

if "count" not in ps: 
    ps.count = 0
if st.button("Increment"):
    ps.count += 1
st.write("This is page1")
st.write(f"count: ", ps.count)
```
最初に`ps = PageSessionState("page1.py")`でページ固有の管理インスタンスを作成して、あとは`st.sessoin_state`の代わりに`ps`を使うだけである。"page1.py"の部分はページ名以外でもよい。

`key`の設定も簡単にできるようにした。
```python
# 普通の書き方
st.text_input("Text input1", key="text_input1")
st.write(st.session_state.text_input1)
# pgssを使った書き方
st.text_input("Text input2", key=ps("text_input2"))
st.write(ps.text_input2)
```

ページ名を追加する部分を隠しただけのシンプルなパッケージであるが、自分にとってはとても便利である。[pgss](https://github.com/masashi2ueda/pgss)は以下のコマンドでインストールできるので、興味があればぜひ試してみてほしい。
```sh
pip install pgss
```