🛖

[Streamlit] ローカル環境か否かで処理を分岐

2024/01/06に公開

もっと良い方法求む!

背景

Streamlitによる開発では個人的に、ローカル環境でのデバッグ時とデプロイ先での本番動作時とで、処理を分岐したいシーンがよくあります。典型的には以下です。

  • ローカルの開発環境では、データの微調整が多いので、キャッシュ (@st.cache_data) を無効にしたい
  • デプロイ先であるCommunity Cloudでは、速度向上のためキャッシュを有効にしたい

手法

  • デコレータ (@st.cache_data) を効かせるかどうか条件分岐可能にする
  • ローカル環境と他の環境をコード上で判定する

雑な疑似コードとしては、こういうことがしたいわけです。

if not is_local_env():
    @st.cache_data
def load_my_huge_dataframe() -> pd.DataFrame:
    ...

Conditionalデコレータ

以下が参考になります。そのまま拝借すれば実現できます。
https://stackoverflow.com/questions/10724854/how-to-do-a-conditional-decorator-in-python

型を付けてみました。

from collections.abc import Callable
from typing import Any


class conditional_decorator:
    def __init__(self, decorator: Callable[[Callable[..., Any]], Callable[..., Any]], condition: bool) -> None:
        self.decorator = decorator
        self.condition = condition

    def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]:
        if self.condition:
            return self.decorator(func)
        return func

使い方です。

import streamlit as st

# デコレータが有効
@conditional_decorator(st.cache_data, True)
def load_my_huge_dataframe1() -> pd.DataFrame():
    ...

# st.cache_data は無視される
@conditional_decorator(st.cache_data, False)
def load_my_huge_dataframe2() -> pd.DataFrame():
    ...

あとはこのTrue/Falseをどう持ってきてあげるかの問題です。

ローカル環境かどうかを判定

以下が参考になりますが、あまり明解な方法は無いようですね。
https://discuss.streamlit.io/t/check-if-run-local-or-at-share-streamlit-io/11841

示されている platform.processor() 以外の方法を以下考えました。Streamlitにおいてローカル環境と他で扱いを変えているといえば、シークレットです。
https://docs.streamlit.io/streamlit-community-cloud/deploy-your-app/secrets-management

.streamlit/secrets.toml ファイルに以下のように書きます。[1]

.streamlit/secrets.toml
local = true

Secretsの手引きにあるように、このTOMLファイルはGitにコミットしないようにしてください。即.gitignoreに入れましょう。

Community Cloudにおいては、Secretsを以下のような画面にて入力することになっています。普通は、ローカル環境の.streamlit/secrets.tomlにある値と同じものを入力しておくわけですが、ここではあえてlocalは登録しません

これによって、ローカル環境とCommunity Cloudとで差を出すことができました。シークレットのlocalエントリの有無で、キャッシュを効かせるかどうかを判定してゴールです。

import streamlit as st

@conditional_decorator(st.cache_data, 'local' not in st.secrets)
def load_my_huge_dataframe() -> pd.DataFrame():
    ...

この類の分岐は、一般には環境変数を使うのが定番の1つだと思います。今回も、ローカル環境での実行時に何らか環境変数をセットすることができるならば、それでも可能だと思います。

$ IS_LOCAL="true" streamlit run main.py
main.py
import os

if os.environ.get("IS_LOCAL"):
    ...
else:
   ...

備考

以下のコードはエラーになります。st.set_page_config は他のst.xxx呼び出しに先んじないといけないそうです。

import streamlit as st

st.write("Hello!")
st.set_page_config(page_title="Title")

そうすると、先ほど示した st.secrets を使う作戦は、メソッド定義を st.set_page_config の後ろに書かないといけない?と不安になるかもしれません。もしそうだとするとかなり不便で、回避のためには奇妙な実装になります。

しかし心配無用のようでした。st.secretsst.set_page_config の前に使っても大丈夫です。

import streamlit as st

print(f"{st.secrets=}")
st.set_page_config(page_title="Title")  # OK

Streamlitのメソッド呼び出しは禁じられているものの、st.secrets は辞書的オブジェクトであり[2]、Streamlitをimportした瞬間にもう評価済みなので、難を逃れたと考えています。

脚注
  1. 値はbooleanでなくても、1でも"foo"でも何でもご自由に。 ↩︎

  2. type(st.secrets)=<class 'streamlit.runtime.secrets.Secrets'> ↩︎

Discussion