いしぐめも

プログラミングとかしたことを書きます。

Streamlit component APIに入門する

「StreamlitってReactっぽいよな~~~」って思いつつ、書けば使えてしまうので何となくで使ってきたStreamlitですが、Streamlitの動作に対して理解を深めるべく Streamlit Componentに入門してみたいと思います。

Streamlitとは

Streamlitはpythonで簡単にフロントエンドを開発できるライブラリです。Pythonのコードを書くだけで、JavaScriptで構成されたフロントエンドとPythonで動くバックエンドを作れてしまう優れものです。

Webで動作するデモアプリを書きたいときにフロントとバックエンドの通信はどうするだとか、各々のサーバーは何で構成するだとか考えなくて済むのでとても重宝します。

Streamlit Component API

Streamlitは「コンポーネント」という単位で画面を構成し、「見出し」「マークダウン」「グラフ」のような感じで何を表示していくかを順に記述していきます。デフォルトでもグラフやDataFrameを表示できるので、十分なぐらいに実装されているのですが、オレオレなコンポーネントを実装したい人向けに「Streamlit Component API」というものが提供されています。

ドキュメントはこちら↓

Components API - Streamlit Docs

自作コンポーネントを作ってみよう

というわけで、自作コンポーネントを作っていきます。極力ステップバイステップで所感を交えつつ手順を紹介したいと思います。

公式から提供されているテンプレートを使う

いきなりテンプレートのコピペとなってしまって申し訳ないですが、公式からコンポーネントのプロジェクトを作るにあたって活用できるテンプレートが提供されているので、ありがたくそれを使っていきます。

github.com

Streamlit公式から提供されているテンプレート「component-template」

画像の通り、「template(React Componentのように記述できる)」「template-reactless(React Componentを使用しないタイプ)」の2つのテンプレートがあります。今回の例ではReact Componentで試したいことがあったので、、前者を利用します。

というわけで、上記 component-template をクローンして、templateフォルダで作業をしていきます。

git clone https://github.com/streamlit/component-template
cd component-template/template

フロントエンド用の資材を準備・サーバー起動

まずは、フロントエンドはTypeScriptで記述したものをトランスパイルして使用するので、依存しているライブラリの導入が必要になります。フロントエンドのフォルダに移動して npm install を実行します。

pushd my_component/frontend
npm install
popd

ライブラリが揃ったら、npm start でサーバーを起動します。Reactユーザーにはお馴染みの react-scripts でWebサーバーが立ち上がります。

pushd my_component/frontend
npm start

開発中は react-scripts で適宜トランスパイルされてWebサーバーで公開されているものを読み取りに行く動きをするようですね。

Streamlitで動作確認する

Streamlit で my_component/__init__.py を実行することで動作確認することができます。

streamlit run my_component/__init__.py

起動すると 8501 番ポートで立ち上がるので、そこにアクセスすると動作している様子を確認することができます。

vscode で devcontainer を使用している場合、vscodeのポートフォワーディングでlocalhostの8501にポートがバインドされるので、ブラウザで http://localhost:8051 を開けばOKです。

VSCodeのポートフォワーディングでコンテナ内のポートにアクセス

Streamlit Component動作確認!

リリースする方法

ソースコードを見てもらえるとわかりますが、_RELEASE 変数の値によって動作が変わるようで、「_RELEASEがFalseの時はローカルサーバーを参照」「_RELEASEがTrueの時はトランスパイル済みのjsファイルを参照」するように動作が変わるみたいです。

なぜわざわざローカルサーバーを参照?ってところですが、これはまぁコンポーネントのソースコートが更新された時にホットリロードされるように、ということなのかなと思います。

_RELEASE をTrueにするとトランスパイル済み資材が参照されるので、これは別途 npm run build でトランスパイルしてそれを含めてモジュール化するという流れになるみたいです。これは公式のワークフローを参照するといいと思います。(ソースコード内の_RELEASE変数書き換えからやってて勉強になります。)

component-template/.github/workflows/ci.yaml at master · streamlit/component-template · GitHub

devcontainer化しました

と、ここまでの内容(+これから以下で試す内容)をdevcontainer化しました。

とはいえ、求められるのはnodeとpythonだけなので、いうほど環境準備はハードル高くないかなと思いますが。ローカル環境汚したくないよって人はぜひ使ってみてください。

GitHub - yoh1496/learning-streamlit-component: Repository for learning streamlit component

気になったこと

個人的に試して気になったことを書きます。(順次書き足していく予定です)

気になったことその1: マウントアンマウントは検知できる?

Streamlit ComponentはReact Componentライクに書くことができるので、Reactのライフサイクル同様、componentDidMount などの関数が呼ばれるかを試してみました。コンポーネントのアンマウント時にクリーンアップなどの処理ができると便利ですよね。

class MyComponent extends StreamlitComponentBase<State> {
  public state = { numClicks: 0, isFocused: false }

  public componentDidMount(): void {
    super.componentDidMount()
    console.log('mount');
  }

  public componentWillUnmount(): void {
    if (super.componentWillUnmount) super.componentWillUnmount();
    console.log('unmount');
  }

  public componentDidUpdate(): void {
    console.log(this.props)
  }

/** 略 **/

というわけでこんな感じに実装して、以下のようにマウント・アンマウントを試すコードを書いてみました。

    if 'mounted' not in st.session_state:
        st.session_state.mounted = False

    def on_click_unmount():
        st.session_state.mounted = False
        del st.session_state.hoge
    
    def on_click_mount():
        st.session_state.mounted = True
    
    mounted = st.session_state.mounted

    st.button('Unmount Component', disabled=not mounted, on_click=on_click_unmount)
    st.button('Mount Component', disabled=mounted, on_click=on_click_mount)

    if mounted:
        my_component('Mount/Unmount Test', key="mount_unmount_test")

session_state を用いてmountedフラグを管理し、mountedフラグの値によって表示/非表示を切り替えるコードです。

結果

これを実行した結果、componentDidMount, componentDidUpdate は呼ばれるが componentWillUnmount は上記pythonスクリプトでは呼ばれないことがわかりました。

この結果では 「非表示にしただけではStreamlit Componentはアンマウントされない」のか「Streamlitではアンマウントに際してcomponentWillUnmount` は呼ばれないのか」は不明ですが、ちょろっとコードを書くだけではアンマウント時の動作を記述することは難しいということは言えそうです。

どなたかアンマウントにあわせて処理を記述する方法をご存じの方がいらっしゃいましたら教えていただければ幸いです。

終わりに

今回はStreamlitをよりカスタマイズするために、Streamlit Componentを試してみました。

冒頭にも書きましたが、Streamlitはちょっとオタメシのフロントエンドを書くのにすごく便利で、Componentと組み合わせることで標準のStreamlitにはない機能を持たせることができることがわかりました。

個人的にはこれからも便利にStreamlitを使っていきたいなと思うところですが、いろいろ複雑なことをやると死ぬ(これについてはまたいつか書きたいです)というところも同時にわかってきたので、あんまりゴチャゴチャComponentで書こうとしてReactで書いた方が結局早くね?とならないように気を付けたいところです。