Reactive References#
Building on previous discussion about the dual roles of a Parameter
- as both a value holder and a metadata container - let’s explore how parameters can go a step further by acting as dynamic references to other parameters (and other, more advanced, reactive references). What we mean by this is that parameters do not have to refer to a specific static value but can reference another object and update reactively when its value changes.
Parameter References#
In the simplest case, when a parameter is configured with allow_refs=True
, it can be given another Parameter
as its value and it will automatically mirror its current value . This capability enables more intricate relationships between parameters, allowing for automatic value synchronization and forming the basis for reactive programming:
import param
class U(param.Parameterized):
a = param.Number()
class V(param.Parameterized):
b = param.Number(default=None, allow_refs=True)
u = U(a=3.14)
v = V(b=u.param.a)
v.b
3.14
By declaring that V.b
allows references we have made it possible to pass the Parameter U.a
, which means v.b
will reflect the value of u.a
:
u.a = 1.57
v.b
1.57
This unidirectional link will be in effect until something else tries to set the value:
v.b = 14.1
u.a = 13.2
v.b
14.1
In other words, if the value is overridden from the outside the link will be automatically removed.
Simple references are resolved when allow_refs=True
but to allow nested references we separately have to set nested_refs=True
.
class W(V):
c = param.List(allow_refs=True, nested_refs=True)
u1 = U(a=3)
u2 = U(a=13)
w = W(c=[u1.param.a, u2.param.a])
w.c
[3, 13]
When we modify either u1.a
or u2.a
, w.c
will update:
u1.a = 7
w.c
[7, 13]
Other reference types#
Note that Parameter
types are not the only types of valid references. The full list of valid references include:
Class and instance
Parameter
objectsFunctions or methods with dependencies added using
param.depends
Reactive Functions using
param.bind
Reactive expressions declared using
param.rx
Custom objects transformed into a valid reference with a hook registered with
param.parameterized.register_reference_transform
.
There are two utility functions which allow resolving all parameters a reference depends on and the current value of the reference:
from param.parameterized import resolve_ref, resolve_value
resolve_ref(u1.param.a), resolve_value(u1.param.a)
([<param.parameters.Number at 0x7f59d0ee44c0>], 7)
Skipping Reference Updates#
Since references are resolved eagerly whenever one of the dependencies change we may run into situations where we want to control when a reference is updated. Specifically we may want to skip resolving a reference if one of the inputs does not meet some condition or only if a certain event is triggered.
Let’s see how we can configure this. Here we will create a class W
with parameters a
and b
and a run
event. We then define a function to add
parameters a
and b
but only if the run
event is active. To do this we can raise a param.Skip
exception in the function.
class W(param.Parameterized):
a = param.Number()
b = param.Number()
run = param.Event()
w = W(a=0, b=2)
def add(a, b, run):
if not run:
raise param.Skip
return a + b
We can now bind all three parameters to the function:
v = V(b=param.bind(add, w.param.a, w.param.b, w.param.run))
v.b
Even though we initialized v.b
with a reference it will not resolve this reference until we trigger a run
event:
w.param.trigger('run')
v.b
2
Caution
Skip
exceptions are a useful tool for handling control flow in an application, however they should only be raised when writing a reactive reference and never be used in place of a real exception in your business logic. For example, when writing a function that fetches some data, you should raise specific exceptions and then catch those in the function that you are using as a reference and raise the Skip
from there, e.g.:
def fetch_data(url):
response = requests.get(url)
json = response.json()
if 'data' in json:
raise ValueError("JSON response did not contain expected 'data' field.")
return json['data']
def data_ref(url):
try:
data = fetch_data(url)
except Exception:
raise Skip
return data
Composable classes through references#
One common problem that occurs when writing classes where one object depends on the parameters of some other object is that the objects end up having to reference each other. This introduces dependencies between the two classes and leads to less composable abstractions and it only gets worse as you add more classes to the mix.
Let’s illustrate this with an example, say we have an investment portfolio and want to determine how much it is worth in our local currency and how much capital gains taxes we owe on our profit. We obtain these values from some external objects called Forex
and TaxSchedule
:
class Forex(param.Parameterized):
exchange_rate = param.Number(default=1.125)
...
class TaxSchedule(param.Parameterized):
capital_gains = param.Number(default=0.25)
...
forex = Forex(exchange_rate=1.14)
tax_schedule = TaxSchedule(capital_gains=0.25)
Without references we would now be forced to structure our Portfolio
class to reference both the forex
and tax_schedule
objects because that’s the only way we could cleanly depend on the values:
class Portfolio(param.Parameterized):
foreign_value = param.Number()
local_value = param.Number()
taxes_owed = param.Number()
forex = param.ClassSelector(class_=Forex)
tax_schedule = param.ClassSelector(class_=TaxSchedule)
@param.depends('foreign_value', 'forex.exchange_rate', watch=True, on_init=True)
def _update_local(self):
self.local_value = self.foreign_value * self.forex.exchange_rate
@param.depends('local_value', 'tax_schedule.capital_gains', watch=True, on_init=True)
def _update_taxes(self):
self.taxes_owed = self.local_value * self.tax_schedule.capital_gains
portfolio = Portfolio(foreign_value=12000, forex=forex, tax_schedule=tax_schedule)
portfolio
Portfolio(foreign_value=12000, forex=Forex(exchange_rate=1.14, name='Forex00009'), local_value=13679.999999999998, name='Portfolio00011', tax_schedule=TaxSchedule(capital_gains=0.25, name='TaxSchedule00010'), taxes_owed=3419.9999999999995)
Not only does our Portfolio
class now have to know about these other objects but it also has to make significant assumptions about it, e.g. the naming of the exchange_rate
and capital_gains
parameter names is now hard coded in the actual class definition.
References allow us to declare only the subset of parameter values our class cares about and does so without having to make any reference to anything external through it, the class becomes agnostic to the exact provider of the value.
class Portfolio(param.Parameterized):
foreign_value = param.Number()
local_value = param.Number()
taxes_owed = param.Number()
exchange_rate = param.Number(allow_refs=True)
capital_gains = param.Number(allow_refs=True)
@param.depends('foreign_value', 'exchange_rate', watch=True, on_init=True)
def _update_local(self):
self.local_value = self.foreign_value * self.exchange_rate
@param.depends('local_value', 'capital_gains', watch=True, on_init=True)
def _update_taxes(self):
self.taxes_owed = self.local_value * self.capital_gains
portfolio = Portfolio(
foreign_value=12000,
exchange_rate=forex.param.exchange_rate,
capital_gains=tax_schedule.param.capital_gains
)
portfolio
Portfolio(capital_gains=0.25, exchange_rate=1.14, foreign_value=12000, local_value=13679.999999999998, name='Portfolio00012', taxes_owed=3419.9999999999995)
As we have discovered reactive references are a powerful tool not only to link two parameters together but unlock the ability to express a whole host of dynamic behavior through a simple, declarative syntax. The various valid reference types make it possible to everything from using generators to push new updates to a parameter periodically or merely in sequence to expressing complex dependencies between two or more parameters with a simple reactive expression. Lastly, they ensure that you can write composable components without incorporating complex dependencies between objects into your classes themselves.