Dependencies and Watchers

As outlined in the Dynamic Parameters guide, Param can be used in multiple ways, including as a static set of typed attributes, dynamic attributes that are computed when they are read (param.Dynamic parameters, a “pull” or “get” model), and using explicitly expressed chains of actions driven by events at the Parameter level (a “push” or “set” model described in this notebook).

Unlike Dynamic Parameters, which calculate values when parameters are accessed, the dependency and watcher interface allows events to be triggered when parameters are set. With this interface, parameters and methods can declare that they should be updated or invoked when a given parameter is modified, spawning a cascading series of events that update settings to be consistent, adapt values as appropriate for a change, or invoke computations such as updating a displayed object when a value is modified. This approach is well suited to a GUI interface, where a user interacts with a single widget at a time but other widgets or displays need to be updated in response. The Dynamic Parameters approach, in contrast, is well suited when Parameters update either on read or in response to a global clock or counter, such as in a simulation or machine-learning iteration.

This user guide is structured as three main sections:

  • Dependencies: High-level dependency declaration via the @param.depends() decorator

  • Watchers: Low-level watching mechanism via .param.watch().

  • Using dependencies and watchers: Utilities and tools for working with events created using either dependencies or watchers.

Dependencies

Param’s depends decorator allows a programmer to express that a given computation “depends” on a certain set of parameters. For instance, if you have parameters whose values are interlinked, it’s easy to express that relationship with depends:

import param

class C(param.Parameterized):
    _countries = {'Africa': ['Ghana', 'Togo', 'South Africa'],
                  'Asia'  : ['China', 'Thailand', 'Japan', 'Singapore'],
                  'Europe': ['Austria', 'Bulgaria', 'Greece', 'Switzerland']}
    
    continent = param.Selector(list(_countries.keys()), default='Asia')
    country = param.Selector(_countries['Asia'])
    
    @param.depends('continent', watch=True)
    def _update_countries(self):
        countries = self._countries[self.continent]
        self.param['country'].objects = countries
        if self.country not in countries:
            self.country = countries[0]

c = C()
c.country, c.param.country.objects
('China', ['China', 'Thailand', 'Japan', 'Singapore'])
c.continent='Africa'
c.country, c.param.country.objects
('Ghana', ['Ghana', 'Togo', 'South Africa'])
c
C(continent='Africa', country='Ghana', name='C00002')

As you can see, here Param updates the allowed and current values for country whenever someone changes the continent parameter. This code relies on the dependency mechanism to make sure these parameters are kept appropriately synchronized:

  1. First, we set up the default continent but do not declare the objects and default for the country parameter. This is because this parameter is dependent on the continent and therefore it is easy to set up values that are inconsistent and makes it difficult to override the default continent since changes to both parameters need to be coordinated.

  2. Next, if someone chooses a different continent, the list of countries allowed needs to be updated, so the method _update_countries() that (a) looks up the countries allowed for the current continent, (b) sets that list as the allowed objects for the country parameter, and (c) selects the first such country as the default country.

  3. Finally, we expressed that the _update_countries() method depends on the continent parameter. We specified watch=True) to direct Param to invoke this method immediately, whenever the value of continent changes. We’ll see examples of watch=False later. Importantly we also set on_init=True, which means that when instance is created the self._update_countries() method is automatically called setting up the country parameter appropriately. This avoids having to declare a __init__ method to manually call the method ourselves and the potentially brittle process of setting up consistent defaults.

Dependency specs

The example above expressed a dependency of _update_countries on this object’s continent parameter. A wide range of such dependency relationships can be specified:

  1. Multiple dependencies: Here we had only one parameter in the dependency list, but you can supply any number of dependencies (@param.depends('continent', 'country', watch=True)).

  2. Dependencies on nested parameters: Parameters specified can either be on this class, or on nested Parameterized objects of this class. Parameters on this class are specified as the attribute name as a simple string (like 'continent'). Nested parameters are specified as a dot-separated string (like 'handler.strategy.i', if this object has a parameter handler, whose value is an object strategy, which itself has a parameter i). If you want to depend on some arbitrary parameter elsewhere in Python, just create an instantiate=False (and typically read-only) parameter on this class to hold it, then here you can specify the path to it on this object.

  3. Dependencies on metadata: By default, dependencies are tied to a parameter’s current value, but dependencies can also be on any of the declared metadata about the parameter (e.g. a method could depend on country:constant, triggering when someone changes whether that parameter is constant, or on country:objects (triggering when the objects list is replaced (not just changed in place as in appending). The available metadata is listed in the __slots__ attribute of a Parameter object (e.g. p.param.continent.__slots__).

  4. Dependencies on any nested param: If you want to depend on all the parameters of a nested object n, your method can depend on 'n.param' (where parameter n has been set to a Parameterized object).

  5. Dependencies on a method name: Often you will want to break up computation into multiple chunks, some of which are useful on their own and some which require other computations to have been done as prerequisites. In this case, your method can declare a dependency on another method (as a string name), which means that it will now watch everything that method watches, and will then get invoked after that method is invoked.

We can see examples of all these dependency specifications in class D below:

class D(param.Parameterized):
    x = param.Number(7)
    s = param.String("never")
    i = param.Integer(-5)
    o = param.Selector(['red', 'green', 'blue'])
    n = param.ClassSelector(param.Parameterized, c, instantiate=False)                    
    
    @param.depends('x', 's', 'n.country', 's:constant', watch=True)
    def cb1(self):
        print(f"cb1 x={self.x} s={self.s} "
              f"param.s.constant={self.param.s.constant} n.country={self.n.country}")

    @param.depends('n.param', watch=True)
    def cb2(self):
        print(f"cb2 n={self.n}")

    @param.depends('x', 'i', watch=True)
    def cb3(self):
        print(f"cb3 x={self.x} i={self.i}")

    @param.depends('cb3', watch=True)
    def cb4(self):
        print(f"cb4 x={self.x} i={self.i}")

d = D()
d
D(i=-5, n=C(continent='Africa', country='Ghana', name='C00002'), name='D00003', o='red', s='never', x=7)

Here we have created an object d of type D with a unique ID like D00003. d has various parameters, including one nested Parameterized object in its parameter n. In this class, the nested parameter is set to our earlier object c, using instantiate=False to ensure that the value is precisely the same earlier object, not a copy of it. You can verify that it is the same object by comparing e.g. name='C00002' in the repr for the subobject in d to the name in the repr for c in the previous section; both should be e.g. C00002.

Dependencies are stored declaratively so that they are accessible for other libraries to inspect and use. E.g. we can now examine the dependencies for the decorated callback method cb1:

dependencies = d.param.params_depended_on('cb1')
[f"{o.inst.name}.{o.pobj.name}:{o.what}" for o in dependencies]
['D00003.x:value',
 'D00003.s:value',
 'D00003.s:constant',
 'D00003.n:value',
 'C00002.country:value']

Here we can see that method cb1 will be invoked for any value changes in d’s parameters x or s, for any value changes in c’s parameter country, and a change in the constant slot of s. These dependency relationships correspond to the specification @param.depends('x', 's', 'n.country', 's:constant', watch=True) above.

Now, if we change x, we can see that Param invokes cb1:

d.x = 5
cb1 x=5 s=never param.s.constant=False n.country=Ghana
cb3 x=5 i=-5
cb4 x=5 i=-5

cb3 and cb4 are also invoked, because cb3 depends on x as well, plus cb4 depends on cb3, inheriting all of cb3’s dependencies.

If we now change c.country, cb1 will be invoked since cb1 depends on n.country, and n is currently set to c:

c.country = 'Togo'
cb1 x=5 s=never param.s.constant=False n.country=Togo
cb2 n=<C C00002>

As you can see, cb2 is also invoked, because cb2 depends on all parameters of the subobject in n.

continent is also a parameter on c, so cb2 will also be invoked if you change c.continent. Note that changing c.continent itself invokes c._update_countries(), so in that case cb2 actually gets invoked twice (once for each parameter changed on c), along with cb1 (watching n.country):

c.continent = 'Europe'
cb1 x=5 s=never param.s.constant=False n.country=Austria
cb2 n=<C C00002>
cb2 n=<C C00002>

Changing metadata works just the same as changing values. Because cb1 depends on the constant slot of s, it is invoked when that slot changes:

d.param.s.constant = True
cb1 x=5 s=never param.s.constant=True n.country=Austria

Importantly, if we replace a sub-object on which we have declared dependencies, Param automatically rebinds the dependencies to the new object:

d.n = C()
cb1 x=5 s=never param.s.constant=True n.country=China
cb2 n=<C C00004>

Note that if the values of the dependencies on the old and new object are the same, no event is fired.

Additionally the previously bound sub-object is now no longer connected:

c.continent = 'Europe'

watch=False dependencies

The previous examples all supplied watch=True, indicating that Param itself should watch for changes in the dependency and invoke that method when a dependent parameter is set. If watch=False (the default), @param.depends declares that such a dependency exists, but does not automatically invoke it. watch=False is useful for setting up code for a separate library like Panel or HoloViews to use, indicating which parameters the external library should watch so that it knows when to invoke the decorated method. Typically, you’ll want to use watch=False when that external library needs to do something with the return value of the method (a functional approach), and use watch=True when the function is side-effecty, i.e. having an effect just from running it, and not normally returning a value.

For instance, consider this Param class with methods that return values to display:

class Mul(param.Parameterized):
    a = param.Number(5,  bounds=(-100, 100))
    b = param.Number(-2, bounds=(-100, 100))

    @param.depends('a', 'b')
    def view(self):
        return str(self.a*self.b)

    def view2(self):
        return str(self.a*self.b)

prod = Mul(name='Multiplier')

You could run this code manually:

prod.a = 7
prod.b = 10
prod.view()
'70'

Or you could pass the parameters and the view method to Panel, and let Panel invoke it as needed by following the dependency chain:

import panel as pn
pn.extension()
pn.Row(prod.param, prod.view)

Panel creates widgets for the parameters, runs the view method with the default values of those parameters, and displays the result. As long as you have a live Python process running (not just a static HTML export of this page as on param.holoviz.org), Panel will then watch for changes in those parameters due to the widgets and will re-execute the view method to update the output whenever one of those parameters changes. Using the dependency declarations, Panel is able to do all this without ever having to be told separately which parameters there are or what dependency relationships there are.

How does that work? A library like Panel can simply ask Param what dependency relationships have been declared for the method passed to it:

[o.name for o in prod.param.params_depended_on('view')]
['a', 'b']

Note that in this particular case the depends decorator could have been omitted, because Param conservatively assumes that any method could read the value of any parameter, and thus if it has no other declaration from the user, the dependencies are assumed to include all parameters (including name, even though it is constant):

[o.name for o in prod.param.params_depended_on('view2')]
['name', 'a', 'b']

Conversely, if you want to declare that a given method does not depend on any parameters at all, you can use @param.depends().

Be sure not to set watch=True for dependencies for any method you pass to an external library like Panel to handle, or else that method will get invoked twice, once by Param itself (discarding the output) and once by the external library (using the output). Typically you will want watch=True for a side-effecty function or method (typically not returning a value), and watch=False (the default) for a function or method with a return value, and you’ll need an external library to do something with that return value.

@param.depends with function objects

The depends decorator can also be used with bare functions, in which case the specification should be an actual Parameter object, not a string. The function will be called with the parameter(s)’s value(s) as positional arguments:

@param.depends(c.param.country, d.param.i, watch=True)
def g(country, i):
    print(f"g country={country} i={i}")

c.country = 'Greece'
g country=Greece i=-5
d.i = 6
cb3 x=5 i=6
cb4 x=5 i=6
g country=Greece i=6

Here you can see that in addition to the classmethods starting with cb previously set up to depend on the country, setting c’s country parameter or d’s i parameter now also invokes function g, passing in the current values of the parameters it depends on whenever the function gets invoked. g can then make a side effect happen such as updating any other data structure it can access that needs to be changed when country or i changes.

Using @param.depends(..., watch=False) with a function allows providing bound standalone functions to an external library for display, just as in the .view method above.

Of course, you can still invoke g with your own explicit arguments, which does not invoke any watching mechanisms:

g('USA', 7)
g country=USA i=7

Watchers

The depends decorator is built on Param’s lower-level .param.watch interface, registering the decorated method or function as a Watcher object associated with those parameter(s). If you’re building or using a complex library like Panel, you can use the low-level Parameter watching interface to set up arbitrary chains of watchers to respond to parameter value or metadata setting:

  • obj.param.watch(fn, parameter_names, what='value', onlychanged=True, queued=False, precedence=0):
    Create and register a Watcher that will invoke the given callback fn when the what item (value or one of the Parameter’s slots) is set (or more specifically, changed, if onlychanged=True). If queued=True, delays calling any events triggered during this callback’s execution until all processing of the current events has been completed (breadth-first Event processing rather than the default depth-first processing). The precedence declares a precedence level for the Watcher that determines the priority with which the callback is executed. Lower precedence levels are executed earlier. Negative precedences are reserved for internal Watchers, i.e. those set up by param.depends. The fn will be invoked with one or more Event objects that have been triggered, as positional arguments. Returns a Watcher object, e.g. for use in unwatch.

  • obj.param.watch_values(fn, parameter_names, what='value', onlychanged=True, queued=False, precedence=0):
    Easier-to-use version of obj.param.watch specific to watching for changes in parameter values. Same as watch, but hard-codes what='value' and invokes the callback fn using keyword arguments param_name=new_value rather than with a positional-argument list of Event objects.

  • obj.param.unwatch(watcher):
    Remove the given Watcher (typically obtained as the return value from watch or watch_values) from those registered on this obj.

To see how to use watch and watch_values, let’s make a class with parameters a and b and various watchers with corresponding callback methods:

def e(e):
    return f"(event: {e.name} changed from {e.old} to {e.new})"

class P(param.Parameterized):
    a = param.Integer(default=0)
    b = param.Integer(default=0)
    
    def __init__(self, **params):
        super().__init__(**params)
        self.param.watch(self.run_a1, ['a'], queued=True, precedence=2)
        self.param.watch(self.run_a2, ['a'], precedence=1)
        self.param.watch(self.run_b,  ['b'])

    def run_a1(self, event):
        self.b += 1
        print('a1', self.a, e(event))

    def run_a2(self, event):
        print('a2', self.a, e(event))

    def run_b(self, event):
        print('b', self.b, e(event))
        
p = P()

p.a = 1
a2 1 (event: a changed from 0 to 1)
a1 1 (event: a changed from 0 to 1)
b 1 (event: b changed from 0 to 1)

Here, we have set up three Watchers, each invoking a method on P when either a or b changes. The first Watcher invokes run_a1 when a changes, and in turn run_a1 changes b. Since queued=True for run_a1, run_b is not invoked while run_a1 executes, but only later once both run_a1 and run_a2 have completed (since both Watchers were triggered by the original event p.a=1).

Additionally we have set a higher precedence value for run_a1 which results in it being executed after run_a2.

Here we’re using data from the Event objects given to each callback to see what’s changed; try help(param.parameterized.Event) for details of what is in these objects (and similarly try the help for Watcher (returned by watch) or PInfo (returned by .param.params_depended_on)).

#help(param.parameterized.Event)
#help(param.parameterized.Watcher)
#help(param.parameterized.PInfo)

Using dependencies and watchers

Whether you use the watch or the depends approach, Param will store a set of Watcher objects on each Parameterized object that let it manage and process Events. Param provides various context managers, methods, and Parameters that help you work with Watchers and Events:

  • batch_call_watchers: context manager accumulating and eliding multiple Events to be applied on exit from the context

  • discard_events: context manager silently discarding events generated while in the context

  • .param.trigger: method to force creation of an Event for this Parameter’s Watchers without a corresponding change to the Parameter

  • Event Parameter: Special Parameter type providing triggerable transient Events (like a momentary push button)

  • Async executor: Support for asynchronous processing of Events, e.g. for interfacing to external servers

Each of these will be described in the following sections.

batch_call_watchers

Context manager that accumulates parameter changes on the supplied object and dispatches them all at once when the context is exited, to allow multiple changes to a given parameter to be accumulated and short-circuited, rather than prompting serial changes from a batch of parameter setting:

with param.parameterized.batch_call_watchers(p):
    p.a = 2
    p.a = 3
    p.a = 1
    p.a = 5
a2 5 (event: a changed from 1 to 5)
a1 5 (event: a changed from 1 to 5)
b 2 (event: b changed from 1 to 2)

Here, even though p.a is changed four times, each of the watchers of a is executed only once, with the final value. One of those events then changes b, so b’s watcher is also executed once.

If we set b explicitly, b’s watcher will be invoked twice, once for the explicit setting of b, and once because of the code self.b += 1:

with param.parameterized.batch_call_watchers(p):
    p.a = 2
    p.b = 8
    p.a = 3
    p.a = 1
    p.a = 5
b 8 (event: b changed from 2 to 8)
a2 5 (event: a changed from 1 to 5)
a1 5 (event: a changed from 1 to 5)
b 9 (event: b changed from 8 to 9)

If all you need to do is set a batch of parameters, you can use .set_param instead of batch_call_watchers, which has the same underlying batching mechanism:

p.param.set_param(a=9,b=2)
b 2 (event: b changed from 9 to 2)
a2 9 (event: a changed from 5 to 9)
a1 9 (event: a changed from 5 to 9)
b 3 (event: b changed from 2 to 3)

discard_events

Context manager that discards any events within its scope that are triggered on the supplied parameterized object. Useful for making silent changes to dependent parameters, e.g. in a setup phase. If your dependencies are meant to ensure consistency between parameters, be careful that your manual changes in this context don’t put the object into an inconsistent state!

with param.parameterized.discard_events(p):
    p.a = 2
    p.b = 9

(Notice that none of the callbacks is invoked, despite all the Watchers on p.a and p.b.)

.param.trigger

Usually, a Watcher will be invoked only when a parameter is set (and only if it is changed, by default). What if you want to trigger a Watcher in other cases? For instance, if a parameter value is a mutable container like a list and you add or change an item in that container, Param’s set method will never be invoked, because in Python the container itself is not changed when the contents are changed. In such cases, you can trigger a watcher explicitly, using .param.trigger(*param_names). Triggering does not affect parameter values, apart from the special parameters of type Event (see below).

For instance, if you set p.b to the value it already has, no callback will normally be invoked:

p.b = p.b

But if you explicitly trigger parameter b on p, run_b will be invoked, even though the value of b is not changing:

p.param.trigger('b')
b 9 (event: b changed from 9 to 9)

If you trigger a, the usual series of chained events will be triggered, including changing b:

p.param.trigger('a')
a2 2 (event: a changed from 2 to 2)
a1 2 (event: a changed from 2 to 2)
b 10 (event: b changed from 9 to 10)

Event Parameter

An Event Parameter is a special Parameter type whose value is intimately linked to the triggering of events for Watchers to consume. Event has a Boolean value, which when set to True triggers the associated watchers (as any Parameter does) but then is automatically set back to False.

Conversely, if events are triggered directly on a param.Event via .trigger, the value is transiently set to True (so that it’s clear which of many parameters being watched may have changed), then restored to False when the triggering completes. An Event parameter is thus like a momentary switch or pushbutton with a transient True value that normally serves only to launch some other action (e.g. via a param.depends decorator or a watcher), rather than encapsulating the action itself as param.Action does.

class Q(param.Parameterized):
    e = param.Event()
    
    @param.depends('e', watch=True)
    def callback(self):
        print(f'e=={self.e}')
        
q = Q()
q.e = True
e==True
q.e
False
q.param.trigger('e')
e==True
q.e
False

Async executor

Param’s events and callbacks described above are all synchronous, happening in a clearly defined order where the processing of each function blocks all other processing until it is completed. Watchers can also be used with the Python3 asyncio async/await support to operate asynchronously. To do this, you can define param.parameterized.async_executor with an asynchronous executor that schedules tasks on an event loop from e.g. Tornado or the asyncio library, which will allow you to use coroutines and other asynchronous functions as .param.watch callbacks.

As an example, you can use the Tornado IOLoop underlying this Jupyter Notebook by putting events on the event loop and watching for results to accumulate:

import param, asyncio, aiohttp
from tornado.ioloop import IOLoop

param.parameterized.async_executor = IOLoop.current().add_callback

class Downloader(param.Parameterized):
    url = param.String()
    results = param.List()
    
    def __init__(self, **params):
        super().__init__(**params)
        self.param.watch(self.fetch, ['url'])

    async def fetch(self, event):
        async with aiohttp.ClientSession() as session:
            async with session.get(event.new) as response:
                img = await response.read()
                self.results.append((event.new, img))

f = Downloader()
n = 7
for index in range(n):
    f.url = f"https://picsum.photos/800/300?image={index}"

f.results
[]

When you execute the above cell, you will normally get [], indicating that there are not yet any results available.

What the code does is to request 10 different images from an image site by repeatedly setting the url parameter of Downloader to a new URL. Each time the url parameter is modified, because of the self.param.watch call, the self.fetch callback is invoked. Because it is marked async and uses await internally, the method call returns without waiting for results to be available. Once the awaited results are available, the method continues with its execution and a tuple (image_name, image_data) is added to the results parameter.

If you need to have all the results available (and have an internet connection!), you can wait for them:

print("Waiting: ", end="")
while len(f.results)<n:
    print(f"{len(f.results)} ", end="")
    await asyncio.sleep(0.05)
    
[t[0] for t in f.results]
Waiting: 0 
0 
0 0 
0 
0 
0 0 
0 
0 
6 
['https://picsum.photos/800/300?image=3',
 'https://picsum.photos/800/300?image=1',
 'https://picsum.photos/800/300?image=0',
 'https://picsum.photos/800/300?image=5',
 'https://picsum.photos/800/300?image=4',
 'https://picsum.photos/800/300?image=6',
 'https://picsum.photos/800/300?image=2']

This while loop iterates until all results are available, printing a count of results so far each time through the loop. Processing is done during the asyncio.sleep call, which returns control to the IOLoop for that length of time, and then the while loop checks to see if processing is done yet. Once it’s done, the list of URLs downloaded is displayed, and you can see from the ordering (unlikely to be sequential) that the downloading was done asynchronously. You can find out more about programming asynchronously in the asyncio docs.

Applying these techniques to your own code

As you can see, there is extensive support for watching for events on Parameters, whether you use the low-level Watcher interface or the high-level @param.depends interface. As usual when multiple APIs are provided, it’s a good idea to start with the high-level interface, and only drop down to the low-level watching approach if you need the additional power and control and are able to accept the corresponding complexity. The asynchronous approach is then only needed for very specific applications where you want your code to be decoupled from an external system. Most people can simply use @param.depends to cover all their needs for interaction between Parameters and for computation that depends on Parameters.