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()
decoratorWatchers: 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(default='Asia', objects=list(_countries.keys()))
country = param.Selector(objects=_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:
First, we set up the default continent but do not declare the
default
for thecountry
parameter. This is because this parameter is dependent on thecontinent
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.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 thecountry
parameter, and (c) selects the first such country as the default country.Finally, we expressed that the
_update_countries()
method depends on thecontinent
parameter. We specifiedwatch=True
to direct Param to invoke this method immediately, whenever the value ofcontinent
changes. We’ll see examples of watch=False later. Importantly we also seton_init=True
, which means that when instance is created theself._update_countries()
method is automatically called setting up thecountry
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:
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)
).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 parameterhandler
, whose value is an objectstrategy
, which itself has a parameteri
). If you want to depend on some arbitrary parameter elsewhere in Python, just create aninstantiate=False
(and typically read-only) parameter on this class to hold it, then here you can specify the path to it on this object.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 oncountry: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__
).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 parametern
has been set to a Parameterized object).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(objects=['red', 'green', 'blue'])
n = param.ClassSelector(default=c, class_=param.Parameterized, 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.method_dependencies('cb1')
[f"{o.inst.name}.{o.pobj.name}:{o.what}" for o in dependencies]
['D00003.x:value',
'D00003.s:value',
'D00003.s:constant',
'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.method_dependencies('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.method_dependencies('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 aWatcher
that will invoke the given callbackfn
when thewhat
item (value
or one of the Parameter’s slots) is set (or more specifically, changed, ifonlychanged=True
). Ifqueued=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). Theprecedence
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 byparam.depends
. Thefn
will be invoked with one or moreEvent
objects that have been triggered, as positional arguments. Returns aWatcher
object, e.g. for use inunwatch
.obj.param.watch_values(fn, parameter_names, what='value', onlychanged=True, queued=False, precedence=0)
:
Easier-to-use version ofobj.param.watch
specific to watching for changes in parameter values. Same aswatch
, but hard-codeswhat='value'
and invokes the callbackfn
using keyword arguments param_name=new_value rather than with a positional-argument list ofEvent
objects.obj.param.unwatch(watcher)
:
Remove the givenWatcher
(typically obtained as the return value fromwatch
orwatch_values
) from those registered on thisobj
.
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.method_dependencies
)).
#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 Event
s. 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 contextdiscard_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.param.watchers
: writable property to access the instance watchersEvent 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 .update
instead of batch_call_watchers
, which has the same underlying batching mechanism:
p.param.update(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)
<param.parameterized._ParametersRestorer at 0x7faa0d2d5f50>
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)
.param.watchers
#
For more advanced purposes it can be useful to inspect all the watchers set up on an instance, in which case you can use inst.param.watchers
to obtain a dictionary with the following structure: {parameter_name: {what: [Watcher(), ...], ...}, ...}
p.param.watchers
{'a': {'value': [Watcher(inst=P(a=2, b=10, name='P00150'), cls=<class '__main__.P'>, fn=<bound method P.run_a1 of P(a=2, b=10, name='P00150')>, mode='args', onlychanged=True, parameter_names=('a',), what='value', queued=True, precedence=2),
Watcher(inst=P(a=2, b=10, name='P00150'), cls=<class '__main__.P'>, fn=<bound method P.run_a2 of P(a=2, b=10, name='P00150')>, mode='args', onlychanged=True, parameter_names=('a',), what='value', queued=False, precedence=1)]},
'b': {'value': [Watcher(inst=P(a=2, b=10, name='P00150'), cls=<class '__main__.P'>, fn=<bound method P.run_b of P(a=2, b=10, name='P00150')>, mode='args', onlychanged=True, parameter_names=('b',), what='value', queued=False, precedence=0)]}}
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. The asynchronous executor can be defined on param.parameterized.async_executor
by default it will start an asyncio event loop or reuse the one that is running. This allows you to use coroutines and other asynchronous functions as .param.watch
callbacks.
As an example we can watch results accumulate:
import param, asyncio, aiohttp
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 7 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 await
ed 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
0 0
0
0 1
6
['https://picsum.photos/800/300?image=5',
'https://picsum.photos/800/300?image=3',
'https://picsum.photos/800/300?image=4',
'https://picsum.photos/800/300?image=1',
'https://picsum.photos/800/300?image=2',
'https://picsum.photos/800/300?image=6',
'https://picsum.photos/800/300?image=0']
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.