PanelでUIの作成#

Panelは、Webベースのダッシュボードインターフェースを開発するための拡張ライブラリです。Bokehが提供するウィジェットとレイアウト要素を基盤としており、Jupyter NotebookやBokehサーバー内でWebユーザーインターフェースとPythonプログラム間のリアルタイムデータ通信を実現します。Panelを使用すると、Webアプリケーションやデータ可視化のダッシュボードインターフェースを迅速に開発できます。

まず、HoloViewsとPanelライブラリを読み込み、pn.extension()を呼び出してPanelのJupyter Notebookプラグインを初期化します。そのパラメータは読み込むプラグイン名で、ここではkatexプラグインとaceプラグインを読み込みます。katexプラグインはLaTeX数式を表示するために使用されます。

import panel as pn
import numpy as np
import holoviews as hv

hv.extension("bokeh")
pn.extension("katex")

UIの作成方法#

次の plot_sin_wave() は、4 つの引数を受け取り、減衰する正弦波の波形データを計算し、holoviews を用いて曲線グラフを返します。本節では、この関数を利用して 4 つの引数を設定する UI を作成し、ウィジェットの値が変更されるとグラフも更新される UI を実装します。

def plot_sin_wave(freq, amp, phase, dump):
    t = np.linspace(0, 10, 400)
    y = amp * np.exp(-dump * t) * np.sin(2 * np.pi * freq * t + np.deg2rad(phase))
    return hv.Curve((t, y)).opts(width=600, show_grid=True)

interact#

次のコードは、interact を使用して plot_sin_wave 関数の引数をインタラクティブに調整できる UI を作成しています。 interact は、関数の引数をインタラクティブに変更できるウィジェットを自動生成します。interact のキーワード引数で、plot_sin_wave 関数の各引数の値の範囲を指定します。

引数の値の指定方法によって、生成されるウィジェットの種類が異なります:

  • タプル (min, max) を指定すると、連続値を調整できるスライダーが作成されます。

  • range やリストを指定すると、離散的な値を選択できるスライダーが作成されます。

ui = pn.interact(
    plot_sin_wave, freq=(1, 10), amp=(1, 10), phase=range(0, 360, 45), dump=(0.0, 1.0)
)
ui

次のコードで作成された UI オブジェクトの構造を表示します。

  • 外側の Column は、全体のレイアウトをまとめるコンテナです。

  • 内部の Column は、各ウィジェットを配置するためのレイアウトです。

  • 内部の Row には、グラフを表す HoloViews オブジェクトが含まれています。

print(ui)
Column(sizing_mode='fixed')
    [0] Column
        [0] IntSlider(end=10, name='freq', start=1, value=5)
        [1] IntSlider(end=10, name='amp', start=1, value=5)
        [2] DiscreteSlider(formatter='%d', name='phase', options=[0, 45, 90, 135, ...], value=0)
        [3] FloatSlider(name='dump', value=0.5)
    [1] Row(sizing_mode='fixed')
        [0] HoloViews(Curve, height=300, name='interactive01348', sizing_mode='fixed', width=600)

ColumnRow などのレイアウトは、objects 属性に入れ子構造の要素を保持します。 次のコードでは、外側のレイアウトを Row に変更し、ウィジェットとグラフを左右に並べます。元の UI のウィジェットとグラフのオブジェクトをそのまま使用するため、新しい UI で値を変更すると、元のグラフも同時に更新されます。

pn.Row(*ui.objects)

コールバック型#

interact() は便利ですが、カスタマイズ性が低いため、複雑な UI を作成する場合には適していません。自由に UI を作成するには、ウィジェットやグラフなどのコンポーネントを個別に作成し、それらを適切に連携させる必要があります。連携方法はいくつかありますが、次のコードでは、UI 開発で最もよく使われるコールバックを利用し、ウィジェットの値の変化に応じてグラフを更新する方法を示します。

❶ ウィジェットの value 属性に対する変更を監視し、その変更が発生するたびに update_figure 関数を実行するように、コールバック関数を登録します。
❷ コールバック関数内で描画関数 plot_sin_wave() を呼び出し、その結果を fig_panel.object に代入して、グラフを更新します。
❸ UI を表示する前に、一度 update_figure() を呼び出して初期のグラフを生成します。

freq = pn.widgets.FloatSlider(value=5, start=1, end=10, name="freq")
amp = pn.widgets.FloatSlider(value=1, start=1, end=5, name="amp")
phase = pn.widgets.Select(
    value=90, options=np.arange(0, 360, 45).tolist(), name="phase"
)
dump = pn.widgets.Spinner(value=0.5, step=0.1, name="dump")

ctrl_panel = pn.Column(freq, amp, phase, dump)
fig_panel = pn.pane.HoloViews()


def update_figure(*events):
    fig_panel.object = plot_sin_wave(
        freq.value, amp.value, phase.value, dump.value
    )  # ❷


for widget in [freq, amp, phase, dump]:
    widget.param.watch(update_figure, "value")  # ❶

update_figure()  # ❸
pn.Row(ctrl_panel, fig_panel)

bind#

コールバックベースのコードは複雑で、メンテナンスしにくいことがあります。Panel では、bind() 関数を提供して、複数のウィジェットとそのコールバック関数を結びつけることができます。これにより、コードが簡潔になり、可読性とメンテナンス性が向上します。

bind()plot_sin_wave 関数をウィジェットの値と結びつけるために使用します。このコードは、plot_sin_wave 関数をウィジェット freq, amp, phase, dump と連携させ、ウィジェットの値が変更されるたびに自動的に関数を再実行させます。

bind() のリターン値は特殊な関数 binded_func で、これを直接 HoloViews パネルに渡すことができます。この関数が実行されると、ウィジェットの値が変更されるたびに自動的にグラフを更新します。

この方法により、ウィジェットとグラフの更新が簡単に結びつけられ、コードがシンプルになります。

freq = pn.widgets.FloatSlider(value=5, start=1, end=10, name="freq")
amp = pn.widgets.FloatSlider(value=1, start=1, end=5, name="amp")
phase = pn.widgets.Select(
    value=90, options=np.arange(0, 360, 45).tolist(), name="phase"
)
dump = pn.widgets.Spinner(value=0.5, step=0.1, name="dump")

ctrl_panel = pn.Column(freq, amp, phase, dump)
binded_func = pn.bind(plot_sin_wave, freq, amp, phase, dump)  # ❶
fig_panel = pn.pane.HoloViews(binded_func)  # ❷
pn.Row(ctrl_panel, fig_panel)

Parameterized#

前の例では、ウィジェットを手動で作成する必要がありました。しかし、param.Parameterized クラスを継承すれば、すべてのパラメータをひとつのクラスにまとめ、パラメータの定義から自動的にウィジェットの UI を生成できます。Param ライブラリについては、本章の最後の節で詳しく説明します。

次のコードでは、param.Parameterized を継承した WaveParam クラスを作成し、param.Numberparam.Selector などを用いて属性を定義します。

WaveParam のオブジェクトを作成します。 ❷このオブジェクトを Row() に直接渡すと、UI ウィジェットが自動的に作成されます。 ❸bind()pars.param['freq'] などのパラメータを渡すと、前の例と同様に、パラメータの値が変更されたときに plot_sin_wave() が実行されます。

import param


class WaveParam(param.Parameterized):
    freq = param.Number(default=5, bounds=(1, 10))
    amp = param.Number(default=1, bounds=(1, 5))
    phase = param.Selector(default=90, objects=np.arange(0, 360, 45).tolist())
    dump = param.Number(default=0.5, step=0.1)


pars = WaveParam()  # ❶
layout = pn.Row(
    pars,  # ❷
    pn.bind(
        plot_sin_wave, *[pars.param[name] for name in ["freq", "amp", "phase", "dump"]]
    ),  # ❸
)
layout

作成された UI の構造は以下のようになります。

print(layout)
Row
    [0] Column(margin=(5, 10), name='WaveParam')
        [0] StaticText(value='<b>WaveParam</b>')
        [1] FloatSlider(end=10, name='Freq', start=1, value=5)
        [2] FloatSlider(end=5, name='Amp', start=1, value=1)
        [3] Select(name='Phase', options=OrderedDict({'0': 0, ...]), value=90)
        [4] FloatInput(name='Dump', value=0.5)
    [1] ParamFunction(function, _pane=HoloViews, defer_load=False)

次のように、グラフを更新する関数を WaveParam クラスのメソッドとして定義することもできます。

bind() の代わりに、depends() を使って WaveParam クラスのメソッドをデコレーションすると、このメソッドと属性を連携できます。
parspars.plotRow に渡せば、UI とグラフを同時に作成できます。

class WaveParam(param.Parameterized):
    freq = param.Number(default=5, bounds=(1, 10))
    amp = param.Number(default=1, bounds=(1, 5))
    phase = param.Selector(default=90, objects=np.arange(0, 360, 45).tolist())
    dump = param.Number(default=0.5, step=0.1)

    @pn.depends("freq", "amp", "phase", "dump")  # ❶
    def plot(self):
        return plot_sin_wave(self.freq, self.amp, self.phase, self.dump)


pars = WaveParam()
layout = pn.Row(pars, pars.plot)  # ❷
layout

フーリエ級数のUI#

この例では、ユーザーはUIを使用して、さまざまな数学関数のフーリエ級数の波形を表示できます。”function”という名前のドロップダウン選択ボックスで関数を選択し、”order”という名前の整数スライダーでフーリエ級数の最高次数を設定します。展開されたフーリエ級数は下部に表示され、フーリエ級数の曲線は右側のチャートに表示されます。

以下は計算部分のプログラムです。calc_fourier(func, order)は数学関数funcをフーリエ級数展開し、その中の最初のorder項を取得します。2つの戻り値があります:フーリエ級数のLaTeXテキストと最初のorder項の曲線(Curve)オブジェクトです。プログラムの計算速度を向上させるために、functools.lru_cache()を使用して計算結果をキャッシュします。

from functools import lru_cache
import numpy as np
from sympy import Piecewise, S, Symbol, And, fourier_series, lambdify, latex, pi


@lru_cache()
def calc_fourier(func, order):
    x = Symbol("x")
    fs = fourier_series(func, (x, 0, 2))
    fs_t = fs.truncate(n=order)
    fs_lambda = lambdify([x], fs_t)
    x_ = np.linspace(0, 4, 200)
    y_ = fs_lambda(x_)
    return f"$${latex(fs_t)}$$", hv.Curve((x_, y_))


x = Symbol("x")

functions = {
    "step": Piecewise((1, x < 1), (0, True)),
    "triangle": Piecewise((x, x < 1), (2 - x, True)),
    "pulse": Piecewise((1, And(S(2) / 3 < x, x < S(4) / 3)), (0, True)),
    "trapezoid": Piecewise(
        (S(3) / 2 * x, x < S(2) / 3), (1, x < S(4) / 3), (3 - S(3) / 2 * x, True)
    ),
    "sawtooth": x,
}

以下に、ステップ関数のフーリエ級数の最初の5項の式とそのグラフを表示します。

from IPython.display import display_latex

formula, curve = calc_fourier(functions["step"], 5)
display_latex(formula, raw=True)
curve.opts(width=600, height=300)
\[\frac{2 \sin{\left(\pi x \right)}}{\pi} + \frac{2 \sin{\left(3 \pi x \right)}}{3 \pi} + \frac{2 \sin{\left(5 \pi x \right)}}{5 \pi} + \frac{2 \sin{\left(7 \pi x \right)}}{7 \pi} + \frac{1}{2}\]

リアクティブ型#

以下に、calc_fourier()のUIを作成します。ここでは、すべてのインターフェース要素をFourierSeriesViewerクラスにラップします。

❶ 2つのウィジェットを作成します。ドロップダウン選択ボックスウィジェットと整数スライダーウィジェットで、それぞれ数学関数名と最高次数を選択できるようにします。
bind()calc_fourier関数と2つのウィジェットを連携させます。
calc_fourier関数は2つの値を返すため、そのままPaneオブジェクトに渡して表示することができません。ここでrx()メソッドで得られたオブジェクトに[]でインデックス操作を加えて、得られた値をLaTeXHoloViewsに渡します。
❹ 最後に、UIを構成するレイアウトオブジェクトを作成します。

class FourierSeriesViewer:
    def __init__(self, functions):
        self.functions = functions
        self.func_name = pn.widgets.Select(name="function", options=self.functions)  # ❶
        self.order = pn.widgets.IntSlider(
            name="order", value=5, start=1, end=10, step=1
        )
        func = pn.bind(calc_fourier, self.func_name, self.order)  # ❷
        self.latex_pane = pn.pane.LaTeX(func.rx()[0])  # ❸
        self.curve_pane = pn.pane.HoloViews(func.rx()[1].opts(width=600, height=300))
        self.layout = pn.Column(  # ❹
            pn.Row(pn.Column(self.func_name, self.order), self.curve_pane),
            self.latex_pane,
        )


fsv = FourierSeriesViewer(functions)
fsv.layout

rx()は、関数や式を「リアクティブ」にするために使用します。リアクティブにすることで、入力が変わった場合に自動的に出力を再計算します。bind()は引数と関数の出力をリアクティブにしますが、rx()はすべてのPythonの演算をリアクティブにすることができます。

例えば、次のfunc()は2つの値を返しますが、pn.bind(func, x=slider).rx()[1]で、この関数の2つ目の返り値とsliderを連携させることができます。

slider = pn.widgets.FloatSlider(name="x", value=5, start=1, end=10)


def func(x):
    return [x + 1, x * 10]


pn.bind(func, x=slider).rx()[1]

コールバック型#

param.Parameterizedの派生クラスのParameter属性を使用すると、ウィジェットの作成とイベント処理を簡素化できます。以下のプログラムでは、param.Parameterizedの派生クラスFourierSeriesViewer2を定義し、2つのParameter属性funcorderを定義します。

初期化メソッド__init__()では、
self.param.func.objectsを使用してfunc属性の候補値を設定します。
❷ UIが表示される前に、latex_panecurve_paneの内容を設定する必要があるため、functionsの最初の値をfunc属性に設定すると、calc()メソッドが実行されます。
param.dependsデコレータを使用してコールバックメソッドcalc()を設定します。calc()メソッドを直接UIに渡さない場合は、watch=Trueを指定する必要があります。こうすることで、funcorderが変化するとcalc()が自動的に実行されます。このメソッド内でcalc_fourier()の戻り値をlatex_pane.objectcurve_pane.objectに設定することで、UIの数式とグラフが更新されます。

import param


class FourierSeriesViewer2(param.Parameterized):
    func = param.Selector()
    order = param.Integer(default=5, bounds=(1, 10))

    def __init__(self, functions, **kw):
        super().__init__(**kw)
        self.param.func.objects = functions  # ❶

        self.latex_pane = pn.pane.LaTeX()
        self.curve_pane = pn.pane.HoloViews()
        self.layout = pn.Column(pn.Row(self, self.curve_pane), self.latex_pane)

        self.func = next(iter(functions.values()))  # ❷

    @param.depends("func", "order", watch=True)  # ❸
    def calc(self):
        order = self.order
        formula, curve = calc_fourier(self.func, order)
        self.latex_pane.object = formula
        self.curve_pane.object = curve.opts(width=500, show_grid=True)


fsv2 = FourierSeriesViewer2(functions)
fsv2.layout.servable()

Panelのクラス類#

Panel のユーザーインターフェースは、次の 3 つの要素で構成されます。

  • Widget: 数値入力ボックス、ボタン、ドロップダウンリストなどの各種ウィジェット要素。

  • Pane: インターフェース内に表示できるオブジェクトをラップする要素。例えば、Bokeh や HoloViews のチャート、Markdown テキストなどを表示できます。

  • Panel: 各種要素のレイアウトを管理する要素。例えば、ウィジェットや Pane を横並び・縦並び・グリッド形式で配置できます。

すべてのWidget類はpanel.widgetsモジュール内で定義されており、panel.widgets.base.Widgetを継承しています。以下に継承関係を示します。

from helper.python import print_subclasses

text = print_subclasses(pn.widgets.base.Widget, return_str=True)
pn.widgets.TextAreaInput(
    value=text, width=500, height=300, styles={"font-family": "monospace"}
)

全てのPane類はpanel.paneモジュール内で定義されており、panel.pane.base.PaneBaseを継承しています。以下に継承関係を示します。

text = print_subclasses(pn.pane.base.PaneBase, return_str=True)
pn.widgets.TextAreaInput(
    value=text, width=500, height=300, styles={"font-family": "monospace"}
)

以下に、一般的な Pane クラスをいくつか挙げます。

  • Markdown: Markdown テキストを HTML に変換して表示

  • HTML: 文字列を HTML として表示

  • DataFrame: Pandas の DataFrame を表として表示

  • LaTeX: LaTeX の数式を表示

  • Video: 動画を再生

  • Audio: 音声を再生

  • Ace: コードエディタを表示

  • HoloViews: HoloViews のチャートを表示

  • Bokeh: Bokeh のチャートを表示

  • ParamFunction / ParamMethod: 関数やメソッドの戻り値に基づいて適切な Pane オブジェクトを選択して表示

全てのPanel類はpanel.layoutモジュール内で定義されており、panel.layout.base.Panelを継承しています。以下に継承関係を示します。

text = print_subclasses(pn.layout.base.Panel, return_str=True)
pn.widgets.TextAreaInput(
    value=text, width=500, height=300, styles={"font-family": "monospace"}
)

Paramてパラメータを定義#

Panelのすべてのクラスはparam.Parameterizedから継承され、すべての属性はParameter属性です。この章の最初のセクションとして、まずParamライブラリの使用方法を簡単に紹介し、以降の章の基礎を築きます。

Python言語の動的特性により、classを使用してクラスを定義する場合、通常__init__()メソッドでインスタンスの各種属性を初期化します。この属性は属性値を保存することしかできません。一方、Paramライブラリを使用して属性を定義する場合、属性値を設定するだけでなく、属性タイプ、値の範囲、説明ドキュメントなどの追加情報を設定することもできます。

Parameter属性#

すべてのParameter属性を持つクラスはparam.Parameterizedクラスから継承されます。Parameter属性はclassの下でparam.Parameterの派生クラスを使用して定義されます。以下の例では、❶Calculatorクラスはparam.Parameterizedクラスから継承され、4つのParameter属性を持っています:op1op2operatorresult。❷op1op2の属性タイプはNumberで、パラメータdefaultstepboundslabelを使用して、それぞれ属性のデフォルト値、ステップサイズ、値の範囲、ラベルテキストを指定しています。❸operatorの属性タイプはSelectorで、この属性の値は候補値リストobjectsのいずれかの要素です。❹resultの属性タイプはStringです。NumberSelectorStringはすべてparam.Parameterの派生クラスで、param.Parameterizedの派生クラスでは、これらのParameterオブジェクトを使用してParameter属性を定義します。

import param


class Calculator(param.Parameterized):  # ❶
    op1 = param.Number(default=2.0, step=0.1, bounds=(-10, 10), label="演算数1")  # ❷
    op2 = param.Number(default=3.0, step=0.1, bounds=(-10, 10), label="演算数2")
    operator = param.Selector(objects=list("+-*/"), label="演算子")  # ❸
    result = param.String(label="結果")  # ❹

Parameter属性を定義するために使用されるすべてのクラスはparam.Parameterから継承されます。次のテーブルには、一般的に使用されるParameter派生クラスの一部がリストされています:

Parameterタイプ

説明

主なキーワードパラメータ

String

文字列

regex allow_None

Number

数値

bounds step

Boolean

ブール値

Filename

ファイルパス

search_paths

List

リスト

class_

Tuple

タプル

length

DataFrame

pandasのDataFrameタイプ

ListSelector

リスト選択

objects

次のコードは、すべてのParameter派生クラスの継承関係が表示されています。

print_subclasses(param.parameterized.Parameter)
└──Parameter
   ├──String
   ├──Dynamic
   │  └──Number
   │     ├──Integer
   │     ├──Magnitude
   │     ├──Date
   │     └──CalendarDate
   ├──Boolean
   │  └──Event
   ├──Tuple
   │  └──NumericTuple
   │     ├──XYCoordinates
   │     └──Range
   │        ├──DateRange
   │        └──CalendarDateRange
   ├──Callable
   │  └──Action
   ├──Composite
   ├──SelectorBase
   │  ├──Selector
   │  │  ├──ObjectSelector
   │  │  ├──FileSelector
   │  │  └──ListSelector
   │  │     └──MultiFileSelector
   │  └──ClassSelector
   │     ├──Dict
   │     │  └──ChildDict
   │     ├──Array
   │     ├──DataFrame
   │     ├──Series
   │     └──Child
   ├──_SignatureSelector
   │  └──Selector
   │     ├──ObjectSelector
   │     ├──FileSelector
   │     └──ListSelector
   │        └──MultiFileSelector
   ├──List
   │  ├──HookList
   │  ├──Children
   │  └──_ListValidateWithCallable
   ├──Path
   │  ├──Filename
   │  └──Foldername
   ├──Color
   ├──Bytes
   ├──Align
   ├──Aspect
   ├──Margin
   └──CallbackException

以下にCalculatorオブジェクトを作成し、その内容を確認します。すべてのParameter属性値が自動的に表示され、属性nameも自動的に追加されていることがわかります。すべてのParameter属性値はデフォルト値です。

c1 = Calculator()
c1
Calculator(name='Calculator01748', op1=2.0, op2=3.0, operator='+', result='')

Parameterizedオブジェクトを作成する際、キーワードパラメータを使用してParameter属性の値を設定できます。例えば:

Calculator(op1=4, op2=5, operator="*")
Calculator(name='Calculator01749', op1=4, op2=5, operator='*', result='')

Parameter属性値の取得と設定は通常の属性と同じです:

c1.op1 = 10
c1
Calculator(name='Calculator01748', op1=10, op2=3.0, operator='+', result='')

Parameter属性にはタイプと値の範囲の検証機能があり、検証に失敗するとValueError例外がスローされます。例外メッセージには例外が発生した理由が表示されます。

try:
    c1.op1 = 100
except ValueError as ex:
    print(ex)

try:
    c1.result = 1.5
except ValueError as ex:
    print(ex)
Number parameter 'Calculator.op1' must be at most 10, not 100.
String parameter 'Calculator.result' only takes a string value, not value of <class 'float'>.

Parameter属性のその他の情報と関連メソッドは、param属性を通じてアクセスできます。以下のプログラムは、属性op1の値の範囲を取得します。c1.param.op1param.Parameter派生クラスNumberのオブジェクトです。

print(f"{c1.param.op1 = }")
print(f"{c1.param.op1.bounds = }")
c1.param.op1 = <param.parameters.Number object at 0x000002E5A2A03580>
c1.param.op1.bounds = (-10, 10)

set_param()メソッドを呼び出すことで、複数の属性値を一度に設定できます:

c1.param.update(op1=10, op2=-3, operator="*")
c1
Calculator(name='Calculator01748', op1=10, op2=-3, operator='*', result='')

Parameterizedクラスは、そのインスタンスオブジェクトとしても使用できます。例えば:

print(f"{Calculator.op1 = }")
try:
    Calculator.op1 = 100
except ValueError as ex:
    print(ex)
Calculator.op1 = 2.0
Number parameter 'Calculator.op1' must be at most 10, not 100.

クラスの`Parameter属性値を変更すると、その属性のデフォルト値も同時に変更されます:

Calculator.op1 = 6
print(f"{Calculator.param.op1.default = }")
Calculator()
Calculator.param.op1.default = 6
Calculator(name='Calculator01750', op1=6, op2=3.0, operator='+', result='')

Parameterオブジェクトの属性per_instanceがデフォルト値Trueの場合、各ParameterizedインスタンスのParameterオブジェクトは独立しています。以下のプログラムは、Calculatorオブジェクトc2を作成し、その属性op1の値の範囲を(-100, 100)に変更します。これにより、c2.op1に100を代入しても例外がスローされなくなります。

c2 = Calculator()
c2.param.op1.bounds = -100, 100
c2.op1 = 100
print(f"{c1.param.op1.bounds = }, {c2.param.op1.bounds =}")
c1.param.op1.bounds = (-10, 10), c2.param.op1.bounds =(-100, 100)

コールバック関数#

Parameter属性に値を代入する際、Parameterizedの派生クラスのオブジェクトは指定されたコールバック関数を呼び出すことができます。以下の例では、❶c1.param.watch()を使用してc1のParameter属性にコールバック関数を追加しています。最初の引数はコールバック関数で、2番目の引数は属性名の文字列または属性名のリストです。onlychangedパラメータがFalseの場合、属性に値を代入するたびにコールバック関数がトリガーされます。Trueの場合、属性値が変更されたときのみコールバック関数がトリガーされます。watch()の戻り値はWatcherオブジェクトです。

コールバック関数を呼び出す際、渡される引数の数は不定であるため、❷コールバック関数callback()は可変引数*eventsを使用してすべての引数値を受け取ります。各引数はEventオブジェクトで、コールバック関数をトリガーしたParameter属性に対応します。❸単一の属性に値を代入する場合、コールバック関数callback()の引数は1つのEventオブジェクトです。❹set_param()メソッドを使用して2つの属性に値を代入する場合、コールバック関数の引数は2つのEventオブジェクトになります。

以下にEventオブジェクトの各属性を示します:

  • what:Parameter属性のどの情報がコールバック関数をトリガーしたか、’value’は属性値がトリガーしたことを示します。

  • name:Parameter属性名

  • obj:コールバック関数をトリガーしたオブジェクト

  • cls:コールバック関数をトリガーしたオブジェクトの型

  • old:代入前の値

  • new:代入後の値

  • type:’set’は代入を示し、’change’は属性値が変更されたことを示します

Parameter属性に値を代入するたびにコールバック関数がトリガーされるため、コールバック関数が時間のかかる場合、プログラムの実行性能に影響を与える可能性があります。❺コンテキストマネージャparam.batch_watch()を使用して、属性の代入をバッチ処理することができます。with文ブロック内では、すべての属性代入がコールバック関数をトリガーしません。with文が終了する際、最後の代入結果を使用してコールバック関数がトリガーされます。❻c1.param.unwatch()を使用して指定されたコールバック関数を登録解除することができます。引数はc1.param.watch()が返すWatcherオブジェクトです。

def callback(*events):  # ❷
    for event in events:
        print(
            f"obj: {event.obj.name if event.obj is not None else None}, ",
            f"what: {event.what}, name: {event.name}, "
            f"old: {event.old}, new: {event.new}, type: {event.type}",
        )
    print()


c1 = Calculator(op1=0, op2=0, name="c1")
watch = c1.param.watch(callback, ["op1", "op2"], onlychanged=False)  # ❶
print("c1.op1 = 5")
c1.op1 = 5  # ❸

print("c1.update(op1=3, op2=6)")
c1.param.update(op1=3, op2=6)  # ❹

print("batch_watch() for loop")
with param.parameterized.batch_call_watchers(c1):  # ❺
    for i in range(8):
        c1.op1 = i
        c1.op2 = i + 1

c1.param.unwatch(watch)  # ❻
print("c1.op1 = 6")
c1.op1 = 6
c1.op1 = 5
obj: c1,  what: value, name: op1, old: 0, new: 5, type: set

c1.update(op1=3, op2=6)
obj: c1,  what: value, name: op1, old: 5, new: 3, type: set
obj: c1,  what: value, name: op2, old: 0, new: 6, type: set

batch_watch() for loop
obj: c1,  what: value, name: op1, old: 6, new: 7, type: set
obj: c1,  what: value, name: op2, old: 7, new: 8, type: set

c1.op1 = 6

watch()メソッドのwhatパラメータを使用して、Parameter属性の他の情報のコールバック関数を指定できます。以下のプログラムは、属性op2の値の範囲にコールバック関数を追加します。値の範囲が変更されると、指定されたコールバック関数が呼び出されます。

c1.param.watch(callback, "op2", what="bounds")
c1.param.op2.bounds = (0, 1000)
obj: None,  what: bounds, name: op2, old: (-10, 10), new: (0, 1000), type: changed

Parameterized派生クラスを定義する際、デコレータparam.depends()を使用してメソッドをコールバックメソッドとして設定することもできます。以下の例では、❶Calculator2Calculatorから継承し、初期化メソッド__init__()で可変引数**kwを使用してすべてのパラメータを受け取ります。❷Parameter属性が正しく割り当てられるように、他の初期化作業を行う前に、まず基底クラスの初期化メソッドを呼び出します。❸self.param.trigger()を使用して属性operatorのコールバックイベントをトリガーします。❹コールバックメソッドはデコレータparam.depends()で定義され、ここではop1op2、およびoperator属性に対してコールバックメソッドrun()を設定しています。パラメータwatchTrueの場合にのみコールバックメソッド関数が追加されることに注意してください。param.depends()で設定されたコールバックメソッドを呼び出す際、パラメータは渡されません。

Calculator2のオブジェクトc2を作成した後、その属性resultはすでに正しく割り当てられています。これは、初期化メソッドでself.param.trigger()を使用してコールバックメソッド関数run()を実行したためです。

class Calculator2(Calculator):  # ❶
    def __init__(self, **kw):
        super().__init__(**kw)  # ❷
        self.param.trigger("operator")  # ❸

    @param.depends("op1", "op2", "operator", watch=True)  # ❹
    def run(self):
        import operator as op

        funcs = {"+": op.add, "-": op.sub, "*": op.mul, "/": op.truediv}
        result = funcs[self.operator](self.op1, self.op2)
        self.result = f"{self.op1:4.1f} {self.operator} {self.op2:4.2f} = {result:4.2f}"


c2 = Calculator2()
print(c2.result)  # ❺
c2.op1 = 5
c2.operator = "*"
print(c2.result)
 6.0 + 3.00 = 9.00
 5.0 * 3.00 = 15.00

自動生成UI#

Parameterized派生クラスは通常、ユーザーインターフェースを自動生成し、インターフェース内の各コントロールがトリガーするイベントに応答するために使用されます。まずPanelライブラリをロードし、extension()を呼び出してNotebookで拡張プラグインを初期化します。Panelライブラリの関連章を参照して、PaneライブラリおよびそのJupyterLabプラグインを正しくインストールしてください。

import panel as pn

pn.extension(inline=False)

panel()を使用してCalculator2オブジェクトc2のユーザーインターフェースを生成します。属性op1op2はインターフェース内の2つのスライダーコントロールに対応します。これは、これらの属性に値の範囲があるためです。属性operatorはインターフェース内のドロップダウン選択ボックスに対応し、属性resultはテキスト入力ボックスに対応します。op1op2、およびoperatorに対応するコントロールが変更されると、オブジェクトc2内の対応する属性値も変更され、run()メソッドが実行されます。run()メソッドで属性resultが設定され、それに対応するテキスト入力ボックスのテキストも即座に更新されます。

p = pn.panel(c2, name="計算器")
p