Understanding Reactivity in pyShiny

Published by onesixx on

https://medium.com/@pevolution.ahmed/understanding-reactivity-in-pyshiny-4e600ab5cf2c

reactivity이란

Gorden Shotwell : https://youtu.be/vanSXTOe7ag?si=iQnjur-y0ACC0yW5의 글
“모든 action에는 등가 반작용(equal and opposite reaction)이 있다”
따라서, “모든 reaction에는 항상 먼저 일어나는 특정 action이 있다”

reactive 프로그래밍
기본적인 경우(아래 2가지)에 반응하는 코드를 작성합니다:

  1. 데이터 자체itself (state)의 변화
  2. 이벤트(기능 또는 발생한 동작)의 변경

사용자가 앱에서의 상호 작용 (데이터를 변경하거나 버튼을 클릭하거나 페이지를 아래로 스크롤하는 등)을 할 때,
반응하는 대화형 애플리케이션을 작성.
이 작업은 백그라운드에서 자동으로 수행되므로 , 사용자는 반응할 대상을 지정하기만 하면 됩니다.

따라서 사용자가 이벤트나 데이터를 변경한 경우,
Shiny에게 해당 이벤트나 데이터를 변경에 대해, 다음 응답으로 해당 기능(기능)을 수행하라고 지시하면 됩니다.

사용자가 UI 입력을 변경할 때마다 Shiny가 이를 어떻게 파악하는지

Shiny behind-the-scenes workflow

Shiny앱이 시작되면 Shiny는 Reactive Graph라고 하는 것을 구성하는데,
간단히 말해 입력과 출력 Shiny 컴포넌트 간의 종속 실행 체인을 설명하는 그래프입니다.

이 지시형 그래프를 사용하여 앱 상태에서 발생할 수 있는 변경 사항을 추적합니다.

Shiny 앱 사용자가 입력 UI 컴포넌트에서 무언가를 변경(데이터 변경)하면
Shiny는 Reactive Graph를 사용하여 변경된 입력 UI 컴포넌트의 종속 실행만 무효화합니다.

따라서 다른 프레임워크처럼 앱 전체를 렌더링하지 않고 앱의 일부만 렌더링하는 지연 로딩을 통해 성능을 향상시킬 수 있습니다.

In brief, what happens during the invalidation process?

Hadley Wacom의 ‘Mastering Shiny ‘에 명시된 대로, 이 과정은 다음 세 단계로 구성됩니다:

1. Reactive Graph에서 변경된 컴포넌트 무효화하기

2. 해당 UI 입력의 모든 종속성을 무효화하도록 알리거나 알립니다.

3. 입력 UI 컴포넌트와 해당 종속성 간의 모든 관계를 제거합니다.

4. 그런 다음 무효화가 완료되면 Shiny가 다시 실행되어 컴포넌트 간의 새로운 관계를 스스로 다시 파악합니다.

reactivity을 사용하는 방법

PyShiny는 앱에서 reactivity을 쉽게 제어할 수 있도록, Decorators 형태의 기능 집합을 제공합니다.
이러한 기능을 제공하는 4가지 주요 함수를 아래 목록과 같이 살펴보겠습니다:

  • @reactive.calc()
  • @reactive.event()
  • @reactive.effect()
  • @reactive.value()

Decorator?

shiny reactivity은 Decorator에 의존하기 때문에, Decorators 구조에 대해 알 필요가 있다.

파이썬 Decorators는 함수의 동작을 향상시키거나 수정할 수 있는 도구벨트라고 생각할 수 있습니다.
목수가 나무 조각의 모양이나 크기를 수정하기 위해 다양한 도구를 사용하는 것처럼, 데코레이터를 사용하면 코드를 변경하지 않고도 함수의 동작을 수정할 수 있습니다.
목수는 도구 벨트를 사용하여 여러 도구를 빠르게 전환하여 다양한 작업을 수행할 수 있으며, Decorators를 사용하면 하나 이상의 함수 동작을 빠르게 수정할 수 있습니다.
공구 벨트가 목수에게 다재다능하고 필수적인 도구인 것처럼 Decorators는 모든 Python 개발자에게 다재다능하고 필수적인 도구입니다.

shiny Decorators와 함께 도구 벨트가 어떻게 작동하는지 살펴봅시다.

@reactive.Calc() Decorator

reactive 계산을 중간 계산 단계로 사용하는 이유는 코드를 작성할 때 반복하지 않기(DRY) 원칙을 지키고 앱이 동일한 작업을 반복적으로 계산하는 데 불필요한 계산 리소스(CPU/RAM)를 소모하는 것을 방지하기 위해서입니다.

한 가지 알아둘 것은 reactive.Calc 데코레이터는, Body에서 사용된 shiny 입력컴포넌트가 변화되는 것을 보았을때,
Reactive Graph를 강제로 무효화하거나 재평가한다는 점입니다.

이는 Shiny에게 입력의 변화를 주시하라고 지시한 다음,
변화가 감지되면 Reactive Graph를 다시 구성하여 이 입력과 그 종속성을 계산하여 반응하도록 하는 것과 같습니다.

ex) 관리자가 펭귄 데이터 집합을 필터링한 다음, (매번 필터링을 계산하지 말고)
필터링된 데이터의 2가지 출력(텍스트로 표시_text_verbatim/ 테이블로 표시_data_frame)하는 앱

from shiny import App, ui, reactive, Session, render
import palmerpenguins
penguins = palmerpenguins.load_penguins()

app_ui = ui.page_fluid(
    ui.input_slider("n", "Input the Year:", 2007, 2009, 1000),
    ui.output_text_verbatim("filtered_txt"),
    ui.output_data_frame("grid"),
)
def server(input, output, session):
    @reactive.Calc
    def func1():
        data = penguins.loc[
            (penguins['species'].isin(['Chinstrap','Adelie'])) & 
            (penguins.year == int(input.n())
        )]
        return data
    
    @output
    @render.text
    def filtered_txt():
        return func1()
    
    @output
    @render.data_frame
    def grid():
        return render.DataGrid(func1(), height="100%", width="100%")

app = App(app_ui, server)

ShinyLive app link

@reactive.Effect Decorator

@reactive.Effect
def _():
@reactive.Effect
@reactive.event(input.btn_effect)
def _():

reactive.Effect는 side effect(부작용)이며,
매개변수 외부의 일부 상태에 의존하거나 수정할 때 이를 부작용이라고 합니다.
ex) 함수가 변수 값을 변경하거나 디스크에 일부 데이터를 쓰는 등

따라서 기본적으로 함수의 외부 범위에서 무언가를 수정하고,
그 수정을 마친 후에는 Reactive Graph가 동일한 수정에 대한 종속성을 취하고,
그 수정에 따라 shiny 입력 중 하나가 변경될 때 값을 반환하지 않고 다시 평가되도록 합니다.

reactive.Calc는 중간 계산 함수이므로, return 값을 반환해야 하고 매개변수를 수정해야 하므로,
중간값 없이 Reactive Graph를 강제로 다시 평가하려면 reactive.Effect를 사용하세요.

다음과 같이 Shiny UI를 reactive으로 수정하고 싶을 때 가장 적합합니다.

  • 업데이트 함수 그룹(ui.update_text, ui.update_slider, ui.update_numeric 등) 사용.
  • 사용자 정의 로직에 따라 ui.insert_ui() 및 ui.remove_ui()를 사용하여 UI 요소 삽입 및 삭제.
  • reactive.Value()를 통해 UI 변경.

다음은 슬라이더 입력이 변경될 때마다 값을 삽입하는 첫 번째 지점을 보여주는 예시입니다.

from shiny import App, render, ui, reactive

app_ui = ui.page_fluid(
    ui.h5("Hover over the number to see it's multiplied value."),
    ui.input_slider("n", "List of the Inserted Numbers: [", 0, 100, 20),
    ui.output_text_verbatim("txt"),
)

def server(input, output, session):
    @reactive.Effect
    def _():
        ui.insert_ui(
            ui.tooltip(
                f"{input.n()}, ", ui.h1(f"Multiple of n={input.n()*2}")
            ),
            ".irs--shiny",
            where="beforeBegin",
        )

    @output
    @render.text
    def txt():
        return f"n*2 is {input.n() * 2}"

app = App(app_ui, server)

Restrict execution

맨처음 무조건 실행되지 않게, @reactive.event(input.btn) 추가

from shiny import App, Inputs, Outputs, Session, reactive, ui
app_ui = ui.page_fluid(
    ui.input_action_button("btn", "Press me!")
)
def server(input: Inputs, output: Outputs, session: Session):


    @reactive.effect
    def _():
        ui.insert_ui(
            ui.p("Number of clicks: ", input.btn()),
            selector="#btn",
            where="afterEnd",
        )
def server(input: Inputs, output: Outputs, session: Session):
    
    @reactive.effect
    @reactive.event(input.btn)
    def _():
        ui.insert_ui(
            ui.p("Number of clicks: ", input.btn()),
            selector="#btn",
            where="afterEnd",
        )
app = App(app_ui, server)

@reactive.event Decorator

액션을 계산하거나 실행하기 전에 (버튼 클릭과 같은) 사용자의 특정 액션을 기다릴 수 있으므로,
앱 사용자가 특정 액션을 취한 후 이벤트가 발생하도록 하려면 reactive.event를 사용합니다.

이렇게 하면 Shiny가 Reactive Graph에서 입력과 입력에 의존하는 다른 reactive 함수 또는 값 사이의 관계를 제거하고 특정 이벤트가 발생할 때만 반응하도록 지시합니다.

사용자가 이벤트를 수행할 때만 Reactive Graph를 재생성하도록 실행을 제한하는 방법이라고 생각하면 됩니다.
따라서 항상 마지막 두 데코레이터(@reactive.Calc, @reactive.Effect) 앞에 사용해야 합니다.

다음은 문서에서 @reactive.event를 사용하는 아주 좋은 예시입니다. 이 예시에서는 reactive.event를 사용하는 세 가지 함수 유형이 있습니다:

값을 UI에 렌더링하는 함수
reactive.event를 사용하여 중간 함수로 reactive.Calc를 트리거하는 함수
reactive.event를 사용하여 reactive.Effect를 사용하여 값을 반환하지 않고 외부 범위 연산인 삽입-투-UI 연산을 트리거하는 함수.

import random

from shiny import App, Inputs, Outputs, Session, reactive, render, ui

app_ui = ui.page_fluid(
    ui.markdown(
        f"""
        This example demonstrates how `@reactive.event()` can be used to restrict
        execution of: (1) a `@render` function, (2) `@reactive.Calc`, or (3)
        `@reactive.Effect`.

        In all three cases, the output is dependent on a random value that gets updated
        every 0.5 seconds (currently, it is {ui.output_ui("number", inline=True)}), but
        the output is only updated when the button is clicked.
        """
    ),
    ui.row(
        ui.column(3,
            ui.input_action_button("btn_out", "(1) Update number"),
            ui.output_text("out_out"),
        ),
        ui.column(3,
            ui.input_action_button("btn_calc", "(2) Show 1 / number"),
            ui.output_text("out_calc"),
        ),
        ui.column(3,
            ui.input_action_button("btn_effect", "(3) Log number"),
            ui.div(id="out_effect"),
        ),
    ),
)


def server(input: Inputs, output: Outputs, session: Session):
    # Update a random number every second
    val = reactive.Value(random.randint(0, 1000))

    @reactive.Effect
    def _():
        reactive.invalidate_later(0.5)
        val.set(random.randint(0, 1000))

    # Always update this output when the number is updated
    @output
    @render.ui
    def number():
        return val.get()

    # Since ignore_none=False, the function executes before clicking the button.
    # (input.btn_out() is 0 on page load, but @@reactive.event() treats 0 as None for
    # action buttons.)
    @output
    @render.text
    @reactive.event(input.btn_out, ignore_none=False)
    def out_out():
        return str(val.get())

    @reactive.Calc
    @reactive.event(input.btn_calc)
    def calc():
        return 1 / val.get()

    @output
    @render.text
    def out_calc():
        return str(calc())

    @reactive.Effect
    @reactive.event(input.btn_effect)
    def _():
        ui.insert_ui(
            ui.p("Random number!", val.get()),
            selector="#out_effect",
            where="afterEnd",
        )


app = App(app_ui, server)

Shinylive link

reactive.Value() Function

지금까지 마지막 세 가지 데코레이터를 사용하여,
사용자의 입력 변경 사항만 관찰하는 reactive 함수를 만드는 방법에 대해 알아보았습니다.

이제 개발자로서 reactive 워크플로를 제어하는 객체를 능동적으로 생성하는 방법에 대해 알아보겠습니다.

reactive.Value는 모든 유형의 데이터를 저장할 수 있는 객체로, 값이 변경되면 해당 값에 의존하는 종속성을 무효화합니다. 이는 shiny 앱에서 (input.x) 같은 shiny 입력을 사용할 때 일어나는 일과 정확히 일치하므로, 보이지 않는 곳에서 shiny 입력은 reactive 값입니다.

ex) reactive 값을 사용하여 버튼을 누르기만 하면 전체 카운터 모듈을 숨기거나 표시하는 방법을 보여주는 예시입니다.

from shiny import App, render, ui, reactive
from .counter import counter_ui, counter_server

app_ui = ui.page_fluid(
    ui.input_action_button("toggle", "Toggle UI"),
    ui.output_ui("toggle_ui"),
)

def server(input, output, session):
    """
    The reactive variable triggers the switching behavior
   (from true to false and vice versa).
    """
    x = reactive.Value(True)

    counter_server("counter1")

    
    @reactive.Effect
    @reactive.event(input.toggle)
    def _():
        x.set(not x())

    @output
    @render.ui
    def toggle_ui():
        app_ui = ui.panel_conditional(
            str(x()).lower(),counter_ui("counter1"),
        )
        return app_ui

app = App(app_ui, server)

Shinylive link for checking the app live.

결론

reactivity를 이해하면 Shiny 프로젝트의 인터랙티브 요소를 더 잘 제어할 수 있습니다. reactivity은 직관적으로 느껴질 때까지 계속 연습해야 하는 요소 중 하나에 불과합니다.

지금까지 reactive 컨트롤 중 네 가지에 대해서만 이야기했지만, 다양한 시나리오에 더 많은 기능을 제공할 수 있는 더 많은 것들이 있습니다. 예를 들어, reactive.poll 데코레이터는 주로 데이터베이스나 파일에서와 같이 외부 데이터 변경을 감시하는 데 사용됩니다.

Categories: shiny

onesixx

Blog Owner

Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x