Understanding Reactivity in Shiny For Python

Published by onesixx on

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

오늘의 주제는 reactivity과 reactive 프로그래밍으로,
Shiny를 경쟁사와 구별하는 핵심 개념 중 하나입니다
(어떻게 다른지 설명하는 Gorden Shotwell의 글을 참조하세요).

“모든 action에는 등가 반작용(equal and opposite reaction)이 있다”

따라서, “모든 reaction에는 항상 먼저 일어나는 특정 action이 있다”고 말하는 것이 논리적이지 않나요?

이제 다시 reactive 프로그래밍으로 돌아가 보겠습니다.
reactive 프로그래밍에서는 아주 기본적인 경우(아래 2가지)에 반응하는 코드를 작성합니다:

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

reactive 프로그래밍은 코드를 작성할 때 가져야 하는 디자인 사고방식이라고 생각할 수 있습니다. 이 사고방식에서는 사용자가 앱에서 데이터를 변경하거나 버튼을 클릭하거나 페이지를 아래로 스크롤하는 등의 상호 작용을 할 때 반응하는 대화형 애플리케이션을 작성합니다.

이 작업은 백그라운드에서 자동으로 수행되므로 , 사용자는 운전대를 잡고 반응할 대상을 지정하기만 하면 됩니다.

따라서 사용자가 이벤트나 데이터를 변경한 경우 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에서 변경된 컴포넌트 무효화하기
  1. 해당 UI 입력의 모든 종속성을 무효화하도록 알리거나 알립니다.
  1. 입력 UI 컴포넌트와 해당 종속성 간의 모든 관계를 제거합니다.

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

Reactivity Controls

이제 reactivity이란 무엇이고
사용자가 UI 입력을 변경할 때마다 Shiny가 이를 어떻게 파악하는지 어느 정도 파악했으니
이제 실제로 reactivity을 사용하는 방법을 알아보겠습니다.

PyShiny는 앱에서 reactivity을 쉽게 제어할 수 있도록
Decorators 형태의 기능 집합을 제공합니다.

이러한 기능을 제공하는 4가지 주요 함수를 아래 목록과 같이 살펴보겠습니다:

  • @reactive.Calc()
  • @reactive.event()
  • @reactive.Effect()
  • @reactive.Value() function

But, first and foremost, what is a Decorator?

파이썬 경험이 없는 분들을 위해 간단히 설명하자면, shiny reactivity은 이러한 기능에 크게 의존하기 때문에 파이썬 Decorators 구조에 대해 언급할 가치가 있다고 생각합니다.

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

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

@reactive.Calc() Decorator

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

따라서 이런 Reactive Graph로 shiny 앱을 만드는 대신에

이렇게 만들 수 있습니다.

예시를 통해 이를 더 잘 이해해 보겠습니다.

관리자가 펭귄 데이터 집합을 필터링한 다음 필터링된 데이터의 두 가지 출력을 출력_text_verbatim 컴포넌트를 사용하여 텍스트로 표시하고 출력_data_frame 컴포넌트를 사용하여 시각적 테이블로 표시하는 멋진 앱을 만들어 달라고 요청했다고 가정해 보겠습니다.

또한 매번 필터링을 계산하지 말라고 했는데, 어떻게 할 건가요?

네, 정답입니다!

reactive.Calc 데코레이터를 사용한 앱의 코드는 다음과 같습니다.

from shiny import App, ui, reactive, Session, render
# pip install palmerpenguins
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.Calc 데코레이터는
Body에서 사용된 shiny 입력컴포넌트가 변화되는 것을 보았을때,
Reactive Graph를 강제로 무효화하거나 재평가한다는 점입니다.

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

@reactive.Effect Decorator

@reactive.Effect
def _():

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

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

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

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

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

  • Using the update functions group (ui.update_text, ui.update_slider, ui.update_numeric, etc).
  • Inserting and deleting UI elements using ui.insert_ui(), and ui.remove_ui() based on custom logic.
  • Changing the UI via a reactive.Value().
  • 업데이트 함수 그룹(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)

Shinylive Link

@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 값입니다.

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

from shiny import App, render, ui, reactive,module
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
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x