[streamlit] Streamlit 실행 모델로 작업하기 - Forms

[출처] https://docs.streamlit.io/develop/concepts/architecture/forms

 

Streamlit Docs

Join the community Streamlit is more than just a way to make data apps, it's also a community of creators that share their apps and ideas and help each other make their work better. Please come join us on the community forum. We love to hear your questions

docs.streamlit.io


사용자가 입력할 때마다 스크립트를 다시 실행하고 싶지 않다면 st.form이 도와드립니다! 폼을 사용하면 사용자 입력을 한 번에 일괄 처리하여 재실행할 수 있습니다. 이 양식 사용 가이드에서는 사용자가 양식과 상호 작용하는 방법을 설명하고 예제를 제공합니다.

1. Example

다음 예제에서는 사용자가 여러 매개변수를 설정하여 맵을 업데이트할 수 있습니다. 사용자가 매개변수를 변경하면 스크립트가 다시 실행되지 않고 맵이 업데이트되지 않습니다. 사용자가 "Update map" 버튼이 있는 form을 제출하면 스크립트가 다시 실행되고 맵이 업데이트됩니다.

사용자가 언제든지 양식 외부에 있는 "Generate new points"을 클릭하면 스크립트가 다시 실행됩니다. 사용자가 양식 내에 제출하지 않은 변경 사항이 있는 경우 재실행 시 해당 변경 사항은 전송되지 않습니다. form에 대한 모든 변경 사항은 form 자체가 제출될 때만 Python 백엔드로 전송됩니다.

import streamlit as st
import pandas as pd
import numpy as np

def get_data():
    df = pd.DataFrame({
        "lat": np.random.randn(200) / 50 + 37.76,
        "lon": np.random.randn(200) / 50 + -122.4,
        "team": ['A','B']*100
    })
    return df

if st.button('Generate new points'):
    st.session_state.df = get_data()
if 'df' not in st.session_state:
    st.session_state.df = get_data()
df = st.session_state.df

with st.form("my_form"):
    header = st.columns([1,2,2])
    header[0].subheader('Color')
    header[1].subheader('Opacity')
    header[2].subheader('Size')

    row1 = st.columns([1,2,2])
    colorA = row1[0].color_picker('Team A', '#0000FF')
    opacityA = row1[1].slider('A opacity', 20, 100, 50, label_visibility='hidden')
    sizeA = row1[2].slider('A size', 50, 200, 100, step=10, label_visibility='hidden')

    row2 = st.columns([1,2,2])
    colorB = row2[0].color_picker('Team B', '#FF0000')
    opacityB = row2[1].slider('B opacity', 20, 100, 50, label_visibility='hidden')
    sizeB = row2[2].slider('B size', 50, 200, 100, step=10, label_visibility='hidden')

    st.form_submit_button('Update map')

alphaA = int(opacityA*255/100)
alphaB = int(opacityB*255/100)

df['color'] = np.where(df.team=='A',colorA+f'{alphaA:02x}',colorB+f'{alphaB:02x}')
df['size'] = np.where(df.team=='A',sizeA, sizeB)

st.map(df, size='size', color='color')

2. User interaction

위젯이 폼에 없는 경우 해당 위젯은 사용자가 값을 변경할 때마다 스크립트 재실행이 트리거됩니다. 키 입력이 있는 위젯(st.number_input, st.text_input, st.text_area)의 경우 사용자가 위젯을 클릭하거나 탭아웃하면 새 값이 스크립트 재실행을 트리거합니다. 사용자가 위젯에 커서가 활성화된 상태에서 Enter 키를 눌러 변경 사항을 제출할 수도 있습니다.

반면 위젯이 양식 내부에 있는 경우 사용자가 해당 위젯을 클릭하거나 탭아웃해도 스크립트가 다시 실행되지 않습니다. 양식 내부의 위젯의 경우 양식이 제출되면 스크립트가 다시 실행되고 양식 내의 모든 위젯이 업데이트된 값을 Python 백엔드로 전송합니다.

키 입력을 허용하는 위젯에서 커서가 활성화되어 있는 경우 사용자는 키보드의 Enter 키를 사용하여 양식을 제출할 수 있습니다. st.number_input 및 st.text_input 내에서 사용자는 Enter 키를 눌러 양식을 제출합니다. st.text_area 내에서 사용자는 Ctrl+Enter/⌘+Enter를 눌러 양식을 제출합니다.

import streamlit as st

# Create two columns
col1, col2 = st.columns(2)

# First column (Outside a form)
with col1:
    st.header("Outside a form")
    
    outside_text_input = st.text_input("Outside text input", "")
    outside_text_area = st.text_area("Outside text area", "")
    
    # Add a horizontal line before showing values from outside
    st.markdown("<hr>", unsafe_allow_html=True)

    # Show the values from outside
    st.header("Values from outside")
    st.write(f"Text input: {outside_text_input}")
    st.write(f"Text area: {outside_text_area}")

# Second column (Inside a form)
with col2:
    
    # Form inside the column
    with st.form("inside_form"):
        st.header("Inside a form")
        inside_text_input = st.text_input("Inside text input", "")
        inside_text_area = st.text_area("Inside text area", "")
        
        # Submit button
        submitted = st.form_submit_button("Submit form")
    
    # Show the values from inside after form submission
    if submitted:
        st.header("Values from inside")
        st.write(f"Text input: {inside_text_input}")
        st.write(f"Text area: {inside_text_area}")

3. Widget values

form이 제출되기 전에는 form 외부의 위젯에 기본값이 있는 것처럼 해당 form 내의 모든 위젯에 기본값이 적용됩니다.

import streamlit as st

with st.form("my_form"):
   st.write("Inside the form")
   my_number = st.slider('Pick a number', 1, 10)
   my_color = st.selectbox('Pick a color', ['red','orange','green','blue','violet'])
   st.form_submit_button('Submit my picks')

# This is outside the form
st.write(my_number)
st.write(my_color)

4. Forms are containers

st.form이 호출되면 프런트엔드에 컨테이너가 생성됩니다. 다른 컨테이너 요소와 마찬가지로 해당 컨테이너에 글을 쓸 수 있습니다. 즉, 위의 예와 같이 파이썬의 with 문을 사용하거나 form 컨테이너를 변수에 할당하고 그 변수의 메서드를 직접 호출할 수 있습니다. 또한 form 컨테이너의 아무 곳에나 st.form_submit_button을 배치할 수 있습니다.

import streamlit as st

animal = st.form('my_animal')

# This is writing directly to the main body. Since the form container is
# defined above, this will appear below everything written in the form.
sound = st.selectbox('Sounds like', ['meow','woof','squeak','tweet'])

# These methods called on the form container, so they appear inside the form.
submit = animal.form_submit_button(f'Say it with {sound}!')
sentence = animal.text_input('Your sentence:', 'Where\'s the tuna?')
say_it = sentence.rstrip('.,!?') + f', {sound}!'
if submit:
    animal.subheader(say_it)
else:
    animal.subheader('&nbsp;')

5. Processing form submissions

form 의 목적은 사용자가 변경하는 즉시 스크립트를 다시 실행하는 Streamlit의 기본 동작을 재정의하는 것입니다. form 외부의 위젯의 경우 논리적 흐름은 다음과 같습니다:

  1. 사용자가 프론트엔드에서 위젯의 값을 변경합니다.
  2. st.session_state와 Python 백엔드(서버)에서 위젯의 값이 업데이트됩니다.
  3. 스크립트 재실행이 시작됩니다.
  4. 위젯에 콜백이 있는 경우 페이지 재실행의 접두사로 실행됩니다.
  5. 재실행 중에 업데이트된 위젯의 함수가 실행되면 새 값을 출력합니다.

양식 내부의 위젯의 경우 사용자가 변경한 내용(1단계)은 양식이 제출될 때까지 Python 백엔드(2단계)에 전달되지 않습니다. 또한 양식 내에서 콜백 함수를 가질 수 있는 유일한 위젯은 st.form_submit_button입니다. 새로 제출된 값을 사용하여 프로세스를 실행해야 하는 경우 세 가지 주요 패턴이 있습니다.

6. Execute the process after the form

form 제출의 결과로 일회성 프로세스를 실행해야 하는 경우 st.form_submit_button에서 해당 프로세스를 조건부로 지정하여 form 다음에 실행할 수 있습니다. 프로세스의 결과를 form 위에 표시해야 하는 경우 컨테이너를 사용하여 form이 출력과 관련하여 표시되는 위치를 제어할 수 있습니다.

import streamlit as st

col1,col2 = st.columns([1,2])
col1.title('Sum:')

with st.form('addition'):
    a = st.number_input('a')
    b = st.number_input('b')
    submit = st.form_submit_button('add')

if submit:
    col2.title(f'{a+b:.2f}')

7. Use a callback with session state

콜백을 사용하여 스크립트 재실행의 접두사로 프로세스를 실행할 수 있습니다.

중요
콜백 내에서 새로 업데이트된 값을 처리할 때는 해당 값을 args 또는 kwargs 매개변수를 통해 콜백에 직접 전달하지 마세요. 콜백 내에서 값을 사용하는 위젯에 키를 할당해야 합니다. 콜백 본문 내의 st.session_state에서 해당 위젯의 값을 조회하면 새로 제출된 값에 액세스할 수 있습니다. 아래 예시를 참조하세요.
import streamlit as st

if 'sum' not in st.session_state:
    st.session_state.sum = ''

def sum():
    result = st.session_state.a + st.session_state.b
    st.session_state.sum = result

col1,col2 = st.columns(2)
col1.title('Sum:')
if isinstance(st.session_state.sum, float):
    col2.title(f'{st.session_state.sum:.2f}')

with st.form('addition'):
    st.number_input('a', key = 'a')
    st.number_input('b', key = 'b')
    st.form_submit_button('add', on_click=sum)

8.Use st.rerun

프로세스가 form 위의 콘텐츠에 영향을 미치는 경우 다른 대안으로 추가 재실행(st.rerun)을 사용하는 방법이 있습니다. 하지만 리소스 효율성이 떨어질 수 있으며 위의 옵션보다 덜 바람직할 수 있습니다.
import streamlit as st

if 'sum' not in st.session_state:
    st.session_state.sum = ''

col1,col2 = st.columns(2)
col1.title('Sum:')
if isinstance(st.session_state.sum, float):
    col2.title(f'{st.session_state.sum:.2f}')

with st.form('addition'):
    a = st.number_input('a')
    b = st.number_input('b')
    submit = st.form_submit_button('add')

# The value of st.session_state.sum is updated at the end of the script rerun,
# so the displayed value at the top in col2 does not show the new sum. Trigger
# a second rerun when the form is submitted to update the value above.
st.session_state.sum = a + b
if submit:
    st.rerun()

9.Limitations

  • 모든 form에는 st.form_submit_button이 포함되어야 합니다.
  • st.button 및 st.download_button은 fomr에 추가할 수 없습니다.
  • st.form은 다른 st.form 안에 삽입할 수 없습니다.
  • 콜백 함수는 form 내의 st.form_submit_button에만 할당할 수 있으며 form의 다른 위젯에는 콜백을 가질 수 없습니다.
  • form 내의 상호 의존적인 위젯은 특별히 유용하지 않을 수 있습니다. 위젯1과 위젯2가 모두 form 안에 있을 때 위젯1의 값을 위젯2에 전달하면 위젯2는 form이 제출될 때만 업데이트됩니다.