Gradioにおけるステート管理を考える
Gradioは簡単にデモアプリを作るときは便利ですが、アプリケーションが複雑になってきたときにステート管理が問題になってきます。ステート用のクラスを用意して、それでラップするのが良さそうだったので試してみました。
目次
はじめに
実践的なアプリケーションだと、コンポーネントが変数(ステート)を複数持っているケースが多いですが、それをどうやって管理すればいいのかかなり迷ったので一つの案を考えてみました。これが正解ではないと思いますが、自分なりにこれはいけるかもなーと思ったので記事にしました。
結論からいうと「そんなにステートフルで複雑なことしたいんだったら、Pythonから離れることも含めて、ちゃんとしたフロントエンドフレームワーク使え」って話はめっちゃあります。ただ、Gradioを使うとフロントエンド部分でかなり楽ができるという、MLもバックエンドもワンオペでやらされている人間からすると切実にありがたいメリットがあるので、今回はGradioでやります。
Gradioにおけるステート
Gradioでセッションの概念を導入するでも書きましたが、単純にローカル変数に書いてしまうと、セッション間で変数が共有されて、ブラウザでレンダリングしたときにグローバル変数的な挙動をします。
例えば、名前を入れてログインボタンを押す→内部で名前が記録する→挨拶ボタンを押すと「◯◯さんこんにちは!」というアプリがあったとします。ローカル変数に直書きしてしまうと、
- ブラウザAで「Joe Biden」という名前を入力し、ログインボタンを押す
- ブラウザBで「Kim Jong-un」という名前を入力し、ログインボタンを押す
- ブラウザAで挨拶ボタンを押すと、「Kim Join-unさんこんにちは!」と表示されてしまう
これを解決するには、gr.Stateを使います。stringのような変数をgr.Stateでラップすればいいです。
ステートが複雑になったとき
先程のケースだと、ステートの変数が名前の1個でしたが、実際は変数が複数だったり、もっと階層的なデータであることが多いです。これを変数複数用意して、
val1 = gr.State("somestring")
val2 = gr.State(123)
のようにだーっと書いてしまうのは変数名が量産されてあまりよろしくありません。
dictでラップしてしまう手もありますが、IDEで型ヒントが効かなかったり、
ステートのクラスを定義してしまう
個人的に行けるかもなーと思ったのがこれです。複数のステートの変数を1つのクラスにまとめてラップしてしまうというやり方です。サンプルコードを以下に示します。
import uuid
import random
import gradio as gr
from typing import Tuple
class StateModel:
def __init__(self):
self.text = ""
self.number = 0
def init(self):
self.text = uuid.uuid4()
self.number = random.randint(0, 9999)
print("--initialize--")
self.show()
def show(self) -> str:
text = {"text": self.text, "number": self.number}
print(text)
return text
def click_button(state: StateModel) -> Tuple[StateModel, str]:
if state.number == 0:
state.init()
else:
state.number += 1
return state, state.show()
with gr.Blocks() as demo:
state = gr.State(StateModel())
button = gr.Button(value="click")
textbox = gr.Textbox()
button.click(click_button, state, [state, textbox])
if __name__ == "__main__":
demo.launch()
このコードでは、複数のステート変数を「StateModel」という1つのクラスに入れています。これは以下のメリットがあります。
- ステート変数が複数になったときに、クラスのインスタンス変数を増やせばいいだけなので拡張が楽
- dictでは型ヒントがでなかったが、Inputの型さえ与えてしまえばIDEでインスタンス変数が補間される
- Gradioでステートをイベントにわたすときは、引数と返り値での受け渡しが必須なので、アプリケーションとして複雑になるとこのへんがごちゃごちゃしがちだが、クラスにまとめてしまえば割りとすっきりする
実際に起動してみる
複数のブラウザをまたいで実行してみたところ、ブラウザ間でステートは独立になってるし、割りと良さそう。
--initialize--
{'text': UUID('0dfad6f2-1de4-4e06-9285-02b19567dcdf'), 'number': 9270}
{'text': UUID('0dfad6f2-1de4-4e06-9285-02b19567dcdf'), 'number': 9270}
{'text': UUID('0dfad6f2-1de4-4e06-9285-02b19567dcdf'), 'number': 9271}
{'text': UUID('0dfad6f2-1de4-4e06-9285-02b19567dcdf'), 'number': 9272}
{'text': UUID('0dfad6f2-1de4-4e06-9285-02b19567dcdf'), 'number': 9273}
{'text': UUID('0dfad6f2-1de4-4e06-9285-02b19567dcdf'), 'number': 9274}
{'text': UUID('0dfad6f2-1de4-4e06-9285-02b19567dcdf'), 'number': 9275}
{'text': UUID('0dfad6f2-1de4-4e06-9285-02b19567dcdf'), 'number': 9276}
--initialize--
{'text': UUID('4c62fc35-9a47-4883-91f6-f92d577304d2'), 'number': 2353}
{'text': UUID('4c62fc35-9a47-4883-91f6-f92d577304d2'), 'number': 2353}
{'text': UUID('4c62fc35-9a47-4883-91f6-f92d577304d2'), 'number': 2354}
{'text': UUID('4c62fc35-9a47-4883-91f6-f92d577304d2'), 'number': 2355}
{'text': UUID('0dfad6f2-1de4-4e06-9285-02b19567dcdf'), 'number': 9277}
{'text': UUID('0dfad6f2-1de4-4e06-9285-02b19567dcdf'), 'number': 9278}
{'text': UUID('4c62fc35-9a47-4883-91f6-f92d577304d2'), 'number': 2356}
複数コンポーネントを1つのクラスにまとめて、その中で状態管理をしたいみたいなことは割りとやるのだけれども、この発想でもうちょい頑張ればクリーンアーキテクチャに近いことはできるかもしれない。
ただ本当にクリーンアーキテクチャにしたいのなら、MVCやMVPみたいなアーキテクチャがしっかり整備されているちゃんとしたフロントエンドフレームワークを使えっていう話。悩ましい。
Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内
技術書コーナー
北海道の駅巡りコーナー