Example for dependencies based on param.watch instead of pn.depends decorator

Hi,

I am a new user of Panel. Many examples on the Panel site use pn.depends
decorator to express callback dependencies on parameters like the example
here:

https://panel.holoviz.org/how_to/param/dependencies.html

It would be much appreciated if someone could rephrase the same
example and show how to use param.watch() watchers
on the “phase” and “frequency” instead. I haven’t been able to make that work.

Ref.:
Dependencies and Watchers — param v1.13.0 (holoviz.org)

Thanks,

Regards,

JM

This might help. Note: pn.bind(..., watch=True) is similar to param.watch(), but more flexible because you can pass as many kwargs/args as you’d like.

import panel as pn

fn_code = pn.widgets.CodeEditor(
    value="""
    import panel as pn
    import pandas as pd
    import hvplot.pandas
    from bokeh.sampledata import stocks

    pn.extension()


    # Callbacks
    def update_plot(ticker, kind):
        df = pd.DataFrame(getattr(stocks, ticker))
        df["date"] = pd.to_datetime(df["date"])
        df = df.set_index("date")
        if kind != "ohlc":
            y = "close"
        else:
            y = None
        return df.hvplot(
            kind=kind,
            y=y,
            grid=True,
            responsive=True,
            height=300,
            title=ticker,
        )


    # Widgets
    ticker_select = pn.widgets.Select(
        name="Stock Ticker", options=list(stocks.__all__), sizing_mode="stretch_width"
    )
    kind_select = pn.widgets.Select(
        name="Plot Type", options=["line", "ohlc", "step"], sizing_mode="stretch_width"
    )
    plot = pn.bind(update_plot, ticker=ticker_select, kind=kind_select)

    # Layout

    widget_row = pn.Row(ticker_select, kind_select)
    pn.Column(widget_row, plot)
    """,
    language="python",
    theme="monokai",
    sizing_mode="stretch_both",
)

cls_code = pn.widgets.CodeEditor(
    value="""
    import param
    import panel as pn
    import pandas as pd
    import hvplot.pandas
    from bokeh.sampledata import stocks

    pn.extension()


    class StockExplorer(param.Parameterized):
        
        # Widgets (Part 1)
        ticker = param.Selector(
            default="AAPL", objects=list(stocks.__all__), doc="Stock ticker symbol"
        )

        kind = param.Selector(
            default="line", objects=["line", "ohlc", "step"], doc="Plot type"
        )

        # Callbacks
        @param.depends("ticker", "kind")
        def update_plot(self):
            df = pd.DataFrame(getattr(stocks, self.ticker))
            df["date"] = pd.to_datetime(df["date"])
            df = df.set_index("date")
            if self.kind != "ohlc":
                y = "close"
            else:
                y = None
            return df.hvplot(
                kind=self.kind,
                y=y,
                grid=True,
                responsive=True,
                height=300,
                title=self.ticker,
            )

        # Widgets (Part 2) and Layout
        def panel(self):
            widget_row = pn.Row(
                *pn.Param(
                    self,
                    widgets={
                        "ticker": {"sizing_mode": "stretch_width"},
                        "kind": {"sizing_mode": "stretch_width"},
                    },
                    show_name=False,
                )
            )
            return pn.Column(widget_row, self.update_plot)

    StockExplorer().panel()
    """,
    language="python",
    theme="monokai",
    sizing_mode="stretch_both",
)
pn.Row(fn_code, cls_code, sizing_mode="stretch_both", min_height=1000).show()

Thanks!

I have used pn.bind already and with a bit more of a success (!) but I was

trying to understand how to make the low level watchers work and get rid
of pn.depends at the same time as recommended in the doc.
I thought the example in the documentation looked fairly easy to modify. I’m using panel 1.2.2 and param 1.13. Don’t know if it has something to do with it.

That said, I will reconsider pn.bind() but still looking for a working param.watch
watching parameters changes in a param.Parameterized class :slight_smile:

Cheers,

Using param.watch should be pretty straight forward, as long as you ensure that the function that is triggered has the right arguments.

Here some basic example:

class Test(param.Parameterized):

    n1 = param.Number()
    n2 = param.Number()

    def __init__(self, **params):
        super().__init__(**params)

        self.param.watch(fn=self.view_watch, parameter_names=['n1', 'n2'])
        self.param.watch_values(fn=self.view_watch_values, parameter_names=['n1', 'n2'])
       
    def view_watch(self, event=None):
        print(f'view_watch({self}, event={event})')

    def view_watch_values(self, **params):
        print(f'view_watch_values({self}, params={params})')
      
test = Test()

test.n1 = 5

view_watch(<Test Test02185>, event=Event(what='value', name='n1', obj=Test(n1=5, n2=0.0, name='Test02185'), cls=Test(n1=5, n2=0.0, name='Test02185'), old=0.0, new=5, type='changed'))
view_watch_values(<Test Test02185>, params={'n1': 5})

Having said that, I typically use the param.watch only if I

  • need to be able to remove a watcher again later in the SW-flow.
  • or activate the watch sometime later

Otherwise @param.depends is just more readable.

Back to your sine example, the following should work (but it shows that for using .watch you now actually have to find a place where to do it (eg. init()):

import panel as pn
import param
import numpy as np

pn.extension()

class Sine(param.Parameterized):

    phase = param.Number(default=0, bounds=(0, np.pi))
    frequency = param.Number(default=1, bounds=(0.1, 2))

    def __init__(self, **params):
        super().__init__(**params)

        self.param.watch(fn=self.view, parameter_names=['phase', 'frequency'])
    
    def view(self, event=None):   ## !!! notice the event arg
        print(f'view({self}, event={event})')
        y = np.sin(np.linspace(0, np.pi * 3, 40) * self.frequency + self.phase)
        y = ((y - y.min()) / y.ptp()) * 20
        array = np.array(
            [list((' ' * (int(round(d)) - 1) + '*').ljust(20)) for d in y])
        return pn.pane.Str('\n'.join([''.join(r) for r in array.T]), height=380, width=500)

sine = Sine(name='ASCII Sine Wave')

pn.Row(sine.param, sine.view)

get rid of pn.depends at the same time as recommended in the doc

Oh! Be careful! We didn’t recommend getting rid of param.depends! Could you point us to the place where you saw that? It seems we need to make our suggestions clearer. What we’ve moved to is to suggest using pn.bind over @pn.depends to create a bound callable that you pass to Panel.

So we used to decorate functions with @pn.depends and pass them to Panel like this:

w1 = pn.widgets.FloatSlider()
w2 = pn.widgets.FloatSlider()

@pn.depends(w1, w2)
def add(a, b):
    return a + b

pn.Column(w1, w2, add)

Instead, now we prefer suggesting relying on pn.bind, we find it cleaner! It helps keeping the business logic separate from the user interface code. It’s also easier for people to see that they can import pretty much any function from external libraries and turn them into interactive functions whose input(s) are totally/partially driven by widgets and output is displayed by Panel.

def multiply(a, b):
    return a * b

pn.Column(w1, w2, pn.bind(multiply, w1, w2))

(If you know how decorators work, you’ll realize that pn.bind is just some syntactic sugar over pn.depends, you could replace pn.bind(multiply, w1, w2) with pn.depends(w1, w2)(multiply).)

Using @param.depends in a Parameterized class like on Declare parameter dependencies — Panel v1.3.5 is perfectly valid. Also, I personally prefer registering top-level callbacks that have side-effects with @pn.depends(..., watch=True) over pn.bind(..., watch=True), since when I write these callbacks they usually involve updating some Panel objects so I don’t have a reason to separate them from my user interface code.

winput = pn.widgets.TextInput()
title = pn.pane.Markdown()

@pn.depends(winput, watch=True)
def update_title(input):
    title.object = f'# {input}'

pn.Column(winput, title)

.param.watch is the lowest level API and we don’t recommend it, unless you have special needs that have partially been listed by @johann , to which I’ll add (not exhaustive!):

  • you need the callback to be triggered if one of the watched Parameter is set to the same value (onlychanged=False)
  • you need more fine grained control over what Parameters were updated (when your watch multiple Parameters with .param.watch the signature of your callback should be def cb(self, *events))
  • you need to compare the old and the new value of a Parameter (event.old and event.new)

Updating the example of @johann to cover the case when multiple Parameters are updated together:

import param

class Test(param.Parameterized):

    n1 = param.Number()
    n2 = param.Number()

    def __init__(self, **params):
        super().__init__(**params)

        self.param.watch(fn=self.view_watch, parameter_names=['n1', 'n2'])
        self.param.watch_values(fn=self.view_watch_values, parameter_names=['n1', 'n2'])
       
    def view_watch(self, *events):
        print(f'view_watch({self}, events={events})')

    def view_watch_values(self, **params):
        print(f'view_watch_values({self}, params={params})')
      
test = Test()

test.param.update(n1=5, n2=2)

# view_watch(<Test Test00004>, events=(Event(what='value', name='n1', obj=Test(n1=5, n2=2, name='Test00004'), cls=Test(n1=5, n2=2, name='Test00004'), old=0.0, new=5, type='changed'), Event(what='value', name='n2', obj=Test(n1=5, n2=2, name='Test00004'), cls=Test(n1=5, n2=2, name='Test00004'), old=0.0, new=2, type='changed')))
# view_watch_values(<Test Test00004>, params={'n1': 5, 'n2': 2})

Johann and Maxime,

Sorry for the late response. I’ve been KO’d by covid after more than 3 years dodging it … :slight_smile:
Your help and comments are very much appreciated. I realize that I obviously mixed up
pn.depends and param.depends. Thanks Maxime for raising the flag on my comment. My mistake. At the same time I feel relieved that @param.depends is valid since it seems to be used in many of the examples.
I will go over all your comments carefully when I’m back in shape. They are
helpful. No, i have no knowledge about decorators other than reading the word in C++ OO pattern books ! This dude is old school, coming from atmospheric physics, fortran 77(!), 90 and MPI world.
But this Holoviz ecosystem looks like a lot of fun for a physicist trying to make sense of atmosphere-ocean interactions !
I hope I can put your great work to some use!

This was the reference I saw!
Ref:
depends Module — Panel v1.2.3 (holoviz.org)

“Despite still being available, usage of pn.depends is no longer recommended, in favor of the less intrusive pn.bind

Hi Johann,

When I run your example I get this:


panel serve johann_sine.py --show
2023-10-11 15:46:42,285 Starting Bokeh server version 3.2.2 (running on Tornado 6.3.3)
2023-10-11 15:46:42,288 User authentication hooks NOT provided (default user enabled)
2023-10-11 15:46:42,290 Bokeh app running at: http://localhost:5006/johann_sine
2023-10-11 15:46:42,290 Starting Bokeh server with process id: 3358755
view(, event=None)
2023-10-11 15:46:47,594 WebSocket connection opened
2023-10-11 15:46:47,595 ServerConnection created


Nothing is displayed in the browser. The sine wave plot and the slider
widgets for phase and frequewncy are displayed on your end ?

JM

Modifying the original sine wave example (Declare parameter dependencies — Panel v1.2.3)
using param.watch_values:

import panel as pn
import param
import numpy as np

pn.extension()

class Sine(param.Parameterized):

    phase = param.Number(default=0, bounds=(0, np.pi))

    frequency = param.Number(default=1, bounds=(0.1, 2))

    def __init__(self, **params):
        super().__init__(**params)
#        self.param.watch(self.view,['phase'])
#        self.param.watch(self.view,['frequency'])        
        self.param.watch_values(fn=self.view,parameter_names=["phase","frequency"])

#self.tabulator.param.watch(self._update_avg_rating, "value")

    
#    @param.depends('phase', 'frequency')
    def view(self,**params):
        print(self.param.frequency,flush=True)
        print(type(self.param.frequency),flush=True)        
        print(self.param.phase,flush=True)
        print(type(self.param.phase),flush=True)
        print(type(self.param['phase']),flush=True)                        
        
        y = np.sin(np.linspace(0, np.pi * 3, 40) * self.param.frequency + self.param.phase)
        y = ((y - y.min()) / y.ptp()) * 20
        array = np.array(
            [list((' ' * (int(round(d)) - 1) + '*').ljust(20)) for d in y])
        return pn.pane.Str('\n'.join([''.join(r) for r in array.T]), height=380, width=500)

sine = Sine(name='ASCII Sine Wave')

pn.Row(sine.param, sine.view).servable()

I get this error:

   y = np.sin(np.linspace(0, np.pi * 3, 40) * self.param.frequency + self.param.phase)
TypeError: unsupported operand type(s) for *: 'float' and 'Number'

I guess it’s only a matter of how to retrieve the actual value (float or int) of the param.Number object ? Using self.param.frequency.value does not work …
Any suggestion. I know this sounds pretty basic ! Sorry.

JM

Sorry I don’t have a lot of type now to look into your example. What I can tell you though is that self.param.frequency is going to return a Parameter object, in that case a Number Parameter. When you want to access its value, you simply need to get self.frequency.

That did it. Thanks!

1 Like