Panel and Param#
import param
import panel as pn
pn.extension()
Param is a Python package that is foundational in the implementation and usage of Panel, and more generally of the HoloViz tools. Param provides super-charged object attributes, called Parameters, that behave like normal Python object attributes but benefit from two major features that are both heavily used in Panel:
Parameter attribute values are runtime validated.
Parameters are objects that can be watched, i.e. you can register callbacks (Python functions or methods) that will be executed when the parameter value changes.
Parameterized and Parameter intro#
A Parameterized class, i.e. a class on which Parameters can be set, is created by inheriting from param.Parameterized
. Most Panel objects (widgets, panels, layouts, templates, etc.) actually inherit from param.Parameterized
!
print(
issubclass(pn.widgets.FloatSlider, param.Parameterized),
issubclass(pn.pane.Matplotlib, param.Parameterized),
issubclass(pn.Column, param.Parameterized),
issubclass(pn.template.BootstrapTemplate, param.Parameterized)
)
True True True True
Let’s create a Parameterized class with a single Parameter.
class A(param.Parameterized):
x = param.Number()
A
has now an attribute x
that can be accessed and set as you would normally do with a plain Python class.
A.x
As each Parameter type has a default value, we can instantiate A
as is.
a = A()
a.x
Obviously, we can set a new value for x
on a
.
a.x = 2
a.x
In Panel documentation we often refer to the Parameter value to describe the attribute value.
The Parameter object, i.e. the object declared in the class body and of type param.Parameter
, can be accessed via the .param
namespace, both at the class and instance level.
A.param.x # equivalent to A.param['x']
a.param.x # # equivalent to a.param['x']
The Parameter object has methods and attributes that can be useful to interact with. We can for instance access the default
value of the x
Parameter.
a.param.x.default
Parameterized classes often don’t need a constructor, because the default constructor already allows them to be configured using Parameters as constructor arguments. Still, sometimes you do need to customize the constructor of a Parameterized class. This happens regularly enough that Param’s users have come up with the following convention that you are likely to encounter.
class A(param.Parameterized):
x = param.Number()
# Naming the keyword arguments `**params` is purely a convention.
def __init__(self, **params):
super().__init__(**params)
self._y = self.x * 2
Attention
For an object attribute to be powered by Param you must declare it as a Parameter! Otherwise, that attribute will become a regular class variable, and as such will be shared across all the instances of that class.
class P(param.Parameterized):
x = param.Number() # Good
w1 = pn.widgets.FloatSlider() # Very likely you DO NOT want this
w2 = param.ClassSelector(class_=pn.widgets.FloatSlider) # Much better!
Runtime type-checking#
The class B
below is created with 4 Parameters:
t
is aNumber
Parameter that only accepts Pythonint
andfloat
values.i
is anInteger
Parameter that only accepts Pythonint
values and that must be within the interval[5, 15]
s
is aString
Parameter that only accepts Pythonstr
values and is documented withdoc
.option
is aSelector
Parameter that only accepts one of the values listed inobjects
.
Note
Param offers many Parameter types. All of them share a common set of arguments like default
or doc
, and some add other arguments such as bounds
for the numerical Parameters.
class B(param.Parameterized):
t = param.Number()
i = param.Integer(default=10, bounds=(5, 15))
s = param.String(default='a string', doc='The simulation name')
option = param.Selector(objects=['a', 'b', 'c'])
Once the class is created, the Parameters are already active and can be updated, but only if they’re valid!
# Updating `t` to 2 is valid
B.t = 2
# However updating `i` with a string isn't and raises an error
try:
B.i = 'bad data'
except Exception as e:
print(e)
Integer parameter 'B.i' must be an integer, not <class 'str'>.
The same principle applies at the instance level, both when creating the instance and when updating it later on.
# Setting `t` to 3 is valid
b = B(t=3)
# However setting `i` to a string isn't and raises an error
try:
B(i='bad data')
except Exception as e:
print(e)
Integer parameter 'B.i' must be an integer, not <class 'str'>.
# Setting `s` to a string is valid
b.s = 'foo'
# However setting `option` to a value not found in `objects` isn't
try:
b.option = 'bad data'
except Exception as e:
print(e)
Selector parameter 'B.option' does not accept 'bad data'; valid options include: '[a, b, c]'
As you can see, code inside of Parameterized objects and code that uses Parameterized objects can often be written without any special error checking, since it knows that only allowed values will ever be present.
Dependencies and watchers#
Parameters are objects that can be watched, i.e., you can register callbacks that will be triggered when their value changes.
The class C
has 3 Parameters. Using the param.depends
decorator, we can declare the callbacks of these classes, i.e. its methods, and the Parameters that they depend on:
updating_on_t
depends on botht1
andt2
.updating_on_s
deponds ons
only.
By setting watch=True
(default: False
), we let Param know that we want it to trigger the callback automatically whenever the Parameters it depends on are updated. Without setting watch
, we’re only declaring the dependency between a callback and its Parameters, which is information that is stored by Param but not used by it otherwise. Libraries like Panel that read this information can then use the declarations to determine their own flow of program execution, as we will see later.
class C(param.Parameterized):
t1 = param.Number(default=2)
t2 = param.Number(default=3)
s = param.String(default='a string')
@param.depends('t1', 't2', watch=True)
def updating_on_t(self):
print(f'New value of t1 and t2: {self.t1}, {self.t2}')
@param.depends('s')
def updating_on_s(self):
print(f'New value of s: {self.s}')
c = C()
Let’s confirm this behavior by setting new values to these Parameters.
c.t1 = 0
New value of t1 and t2: 0, 3
c.t2 = 1
New value of t1 and t2: 0, 1
c.s = 'another string'
Param offers a more low-level API to set up watchers, with the watch
function available on the .param
namespace. watch
accepts a callback and a list of Parameter names, and a few other arguments. The callback will receive an Event
object, and the new Parameter value can be found on its new
attribute.
def callback(event):
print(event)
print()
print(f'Old value of s: {event.old}')
print(f'New value of s: {event.new}')
# Setting up a watcher that will trigger the callback when `s` changes.
c.param.watch(callback, ['s'])
c.s = 'new string'
Event(what='value', name='s', obj=C(name='C00119', s='new string', t1=0, t2=1), cls=C(name='C00119', s='new string', t1=0, t2=1), old='another string', new='new string', type='changed') Old value of s: another string New value of s: new string
Panel and Param#
Panel knows how to map Parameters to widgets, and so it can easily generate a set of widgets from a Parameterized class that control its Parameters and trigger any dependent callbacks:
class D(param.Parameterized):
t = param.Number()
i = param.Integer(default=10, bounds=(5, 15))
s = param.String(default='a string', doc='The simulation name')
option = param.Selector(objects=['a', 'b', 'c'])
@param.depends('t', 'i')
def compute(self):
return self.t * self.i
d = D()
pn.panel(d.param)
Panel, when given a method decorated with @param.depends
, will re-run the method and render its new output every time one of the Parameters it depends on change:
pn.Row(d.param.t, d.param.i, d.compute)
Because the displayable objects provided by Panel are all Parameterized objects with their own set of Parameters, they can all be watched. We can for instance hide a widget (setting widget.visible
to False
) by watching the value of another widget:
checkbox = pn.widgets.Checkbox(value=True)
slider = pn.widgets.FloatSlider()
def hide(event):
slider.visible = event.new
checkbox.param.watch(hide, 'value')
pn.Row(checkbox, slider)
Using .param.watch
as done just above is a valid (albeit pretty verbose!) way to set up some interactivity between Panel components. Panel also provides a much more natural “reactive” API, allowing you to bind the value of two Parameters together, or to bind the value of a Parameter to a callback that depends on some additional Parameters. In the example below, tinput.visible
and output.visible
will be updated whenever checkbox.value
changes (clicking on the checkbox), and the value of output.object
will be updated whenever tinput.value
changes, that value being transformed by the boldit
callback.
checkbox = pn.widgets.Checkbox(value=True)
tinput = pn.widgets.TextInput(value='some text always bold', visible=checkbox.param.value)
def boldit(value):
return f'**{value}**'
output = pn.pane.Markdown(object=pn.bind(boldit, tinput.param.value), visible=checkbox.param.value)
pn.Row(checkbox, tinput, output)
As you can see, Parameters offer a very generic mechanism for your Python code to declare options that control it, and Panel’s Param support allows you to work naturally with any combination of objects and their Parameters.