FlaskからStreamlitに乗り換える話　～もちろん湯婆婆もあるよ～

# はじめに
前回、私はFlask単体で実現できないことをAjaxを使うという奇策でなんとか実装させた。

https://qiita.com/mo256man/items/d3f1761e8454d1bd8305

だが、これが王道のやり方だとは思えず、投稿後もほかの方法を模索していた。
Djangoならできるのか。Flaskより高性能だが難解という評価ゆえ私が選ばなかったDjangoなら。いや、DjangoもJinja2を使うとFlask勉強中に読んだことがある。ならばとりあえずはパスだ。
ほかにいい方法はないだろうか。そう考えて思い出したのが、勉強会で仲間がLTしてくれた手法、**Streamlit**だった。


# Streamlit
Streamlitはデータの可視化に特化したフレームワーク。ただし今回はグラフ化については触れない。
テキストの修飾にはマークダウンを使うのでこうやってQiitaに投稿している人はそう苦労せずに作ることができるようになるだろう。
起動にはPythonスクリプトの実行（IDEでのF5）ではなく`streamlit run ファイル名`というコマンドを実行する必要がある。

## 湯婆婆みたび
サンプルプログラムは湯婆婆で書く。これが令和のHello worldだ。

### せっかちすぎる湯婆婆

まずはinputとprintに相当する関数だけで作ってみよう。

```python:yubaba1.py
import streamlit as st
import random

name = st.text_input("契約書だよ。そこに名前を書きな。")
new_name = random.choice(name)

st.write(f"フン。{name}というのかい。贅沢な名だねぇ。")
st.write(f"今からお前の名前は{new_name}だ。")
st.write(f"いいかい、{new_name}だよ。")
st.write(f"## 分かったら返事をするんだ、{new_name}!!")  # writeはマークダウンも書ける
st.write("---")                                      # 水平線
st.write("<div style='text-align: right;'>制作・著作　Y・B・B</div>",
         unsafe_allow_html=True)                     # htmlを書くにはこうする
```

結果はこう。実行するといきなりエラーになる。
![yubaba11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/f23d9fe7-1884-1ea1-910c-9bb5a322dd04.png)


湯婆婆er（ゆばーばー）にはお馴染みのエラーだが、いくら[湯婆婆の元祖が空白時の処理がなくてクラッシュする仕様](https://qiita.com/Nemesis/items/c7192a7c510788d2cba2)だといっても「ヌルストリングを入力」でなく入力決定していない起動直後の状態でこうなってしまうのは嬉しくない。

実はStreamlitのtext_input関数は入力を待ってくれないのだ。そのためこの関数は初期値を設定することができる。ただしブール値など文字列でないものを初期値に指定しても文字列として扱われてしまうのでプログラム起動直後とヌルストリング入力後を区別することはできなかった。



一応、エラー状態でも気にせずにインプットボックスに名前を入力すれば続行してくれる。
![yubaba12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/de6ffa24-6395-222e-aad8-c045d1bf8bd8.png)


### 例外を拾う湯婆婆

次にtry ～ exceptで例外を拾ってみよう。

```python:yubaba2.py
import streamlit as st
import random

name = st.text_input("契約書だよ。そこに名前を書きな。")
try:
    new_name = random.choice(name)
except Exception as e:
    new_name = str(e)
st.write(f"フン。{name}というのかい。贅沢な名だねぇ。")
st.write(f"今からお前の名前は{new_name}だ。")
st.write(f"いいかい、{new_name}だよ。")
st.write(f"### 分かったら返事をするんだ、{new_name}!!")
st.write("---")
st.write("<div style='text-align: right;'>制作・著作　Y・B・B</div>",
         unsafe_allow_html=True)
```

結果はこう。
![yubaba13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/4df663d3-3e97-150b-7028-503421fd5213.png)

プログラムは完走してくれるが、こちらが名前を入力する前から例外を拾ってしまっている点は変わらない。
この、焼き芋を食べる前におならをするがごとき挙動はどうすれば直せるのだろうか。


### 起動直後とそれ以外を区別する湯婆婆

Streamlitは人が操作するたびスクリプトが再実行され変数の値もリセットされてしまう。それを避けるにはsession_stateという予約された辞書を使う。これによりセッション内の変数の値を保持することができる。なお、ブラウザ再読み込みでsession_stateもリセットされる。
プログラム起動直後はsession_stateは空なのでそれを判定すれば`random.choice()`やその結果としてのエラーとは別の初期値を設定することができる。

以下のコードは関数内のローカル変数とメインルーチン内の変数と辞書のキーを全部同じ名称にしてしまったため若干分かりづらくなってしまったががんばって読み解いてほしい。

```Python:yubaba3.py
import streamlit as st
import random

def answer():
    try:
        new_name = random.choice(st.session_state["name"])
    except Exception as e:
        new_name = str(e)
    finally:
        st.session_state["new_name"] = new_name

if not any(st.session_state):   # 初期状態（辞書が空）だと実行される
    st.session_state["name"] = ""
    st.session_state["new_name"] = False

name = st.text_input("契約書だよ。そこに名前を書きな。", 
                    key="name", on_change=answer)
new_name = st.session_state["new_name"]

if new_name:                    # 初期状態（False）だとスルーされる
    st.write(f"フン。{name}というのかい。贅沢な名だねぇ。")
    st.write(f"今からお前の名前は{new_name}だ。")
    st.write(f"いいかい、{new_name}だよ。")
    st.write(f"### 分かったら返事をするんだ、{new_name}!!")

st.write("---")
st.write("<div style='text-align: right;'>制作・著作　Y・B・B</div>",
         unsafe_allow_html=True)
```

こうすることで、起動時は

![yubaba14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/6eee7eb5-91c5-d836-498c-54419a192416.png)

となっており、名前を入力すると

![yubaba15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/c57a538c-552f-ac0b-13d7-6f9c9e35a9f0.png)

と返答してくれて、その後（起動直後は不可。初期値がヌルなのでEnterを押してもon_changeが発火しない）空白を入力すると

![yubaba16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/6b9140de-4949-a9b8-b026-386e05e1c0a3.png)

と例外を拾ってくれるようになった。


## その他の便利な機能

### 要素を更新する
Streamlitで要素を更新するには空のコンテナを定義してそれを書き換えるようにする。ループ内に普通のStreamlit構文を書くとどんどん追記されてしまう。
繰り返し実行される湯婆婆でなぜこれが必要なかったかというと、名前入力のたびにスクリプトが再実行され画面が再描写されているからだ。

```Python:どんどん追記される例
import streamlit as st
import time
for i in range(10):
    st.write(f"羊が{i+1}匹")
    time.sleep(1)
st.write("zzz...")
```

```Python:都度更新される例
import streamlit as st
import time
msg = st.empty()            # 空のコンテナを用意する
for i in range(10):
    msg.write(f"羊が{i+1}匹")
    time.sleep(1)
msg.write("zzz...")
```

### 要素を横に並べる
Streamlitで要素を横に並べるには`st.column()`を使う。整数を指定すれば同じ幅のカラムが指定した数だけ作られるし、各要素が数値のリストを指定すればそれぞれの値の比率の幅を持つカラムが用意される。
現時点ではカラムの中にカラムを置くことはできない。

```Python:2列
import streamlit as st
col1, col2 = st.columns(2)  # 2列のコンテナを用意する
with col1:
    st.write("なんとか")
with col2:
    st.write("かんとか")
```

SharePointでサイトを作るときも似たような仕組みだった。イマドキのWebサイトはレスポンシブに対応しているのが当然で、縦長画面のスマホでも見れるようコンテンツは横に並べるのでなく下に下に配置していくものという思想があるのだろう。

### 画像を表示させる

Streamlitで画像を表示させるには`st.image()`を使う。ここで指定するのはnumpy配列でも良いし文字列でも良いしそれらを含むリストでも良い。
具体的には`numpy.array()`変換されていない素のPILイメージでもOKだし、OpenCVでも可。ただしカラーはRGBで解釈するのでOpenCVの場合はBGR2RGB変換をする必要がある。文字列はURLとかローカルのファイルパスとか。RGBA画像やアニメGIFも可。リストで指定すれば複数画像を並べて表示してくれる。


# 本題

以上の技術を使って[前回の記事](https://qiita.com/mo256man/items/d3f1761e8454d1bd8305)と同等のことを実現しようとしたら。あっさりできてしまった。

## 成果物
前回実装した画像以外のデータのリアルタイム更新はできて当然なので省略している。

![yobikomi2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/8a1477dd-1bc5-a46f-a00e-e30f06a1d99a.gif)

## ソース
前回はFlask＋AjaxだったのでPython書いてhtml書いてJavaScript書いてと盛りだくさんだったが、今回はPythonのコードだけでできている。しかも行数は前回のPythonと同程度。この簡便さ、実にありがたい。

<details><summary>折りたたみ</summary>


```Python:stream_with_streamlit.py
import streamlit as st
import cv2

def mosaic(img):
    h, w = img.shape[:2]
    img = cv2.resize(img, (w//10, h//10))
    img = cv2.resize(img, (w, h), interpolation=cv2.INTER_NEAREST)
    return img

def main():
    cap = cv2.VideoCapture(0)
    col1, col2 = st.columns(2)
    with col1:
        st.write("#### 元画像")
        st_img1 = st.empty()
    with col2:
        st.write("#### モザイク")
        st_img2 = st.empty()

    while True:
        ret, frame = cap.read()
        if ret:
            frame = frame[:, :, ::-1]   # BGR -> RGB
            st_img1.image(frame)
            st_img2.image(mosaic(frame))

if __name__ == "__main__":
    main()
```
</details>


# 終わりに
これですっかりStreamlitが好きになってしまった。
老害センスで黒い枠線を描画したくなる気持ちはあるが、そんなのは些細なこと。[公式サイトのギャラリー](https://streamlit.io/gallery)を見てデータ可視化や分析に活用していこう。