Can a paramterized class react to parameters outside of itself?

Complex applications can have deeply nested component hierarchies. Along the way, we may want to encapsulate state at different levels of the component hierarchy, say parent and child components. Further, the children of some containing component may want to react to changes in their parents state. Parameterized classes are the only way to share a components internal state with other components, but its not clear what the best practice is for sharing that state to children in a reactive way.

I’ve come up with the following, but it is ugly and won’t scale well:

import param
import panel as pn
pn.extension()

class StatefulComponent(param.Parameterized):
    name = param.String()

    def __init__(self, name, children, **params):
        super().__init__(**params)
        self.name = name 
        self.children = children

    # This is ugly, do I really have to do this?
    @param.depends('name')
    def update_subcomponent(self):
        self.children.parents_name = self.name

    def view(self):
        return pn.pane.Markdown(f"I am {self.name}")

class StatefulSubcomponent(param.Parameterized):
    name = param.String()
    # This is ugly too, I have to maintain a 
    # copy of the parent parameter to retain reactivity
    parents_name = param.String()

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

    def view(self):
        return pn.pane.Markdown(
            f'I am {self.name}. My parent is {self.parents_name}')


sub = StatefulSubcomponent(name='Chapter')
main = StatefulComponent(name='Book', children=sub)
pn.Column(
    main.view, 
    sub.view, 
    main.update_subcomponent # This is also ugly
).servable() 

If you create a parameter that references the parent you can directly declare the dependency, e.g. here I’ve added a parent parameter, which I assign to. Now that we have made that declaration we can use param.depends to declare the dependency on the name parameter of the parent:

import param
import panel as pn
pn.extension()

class StatefulComponent(param.Parameterized):
    name = param.String()

    def __init__(self, name, child, **params):
        super().__init__(**params)
        self.name = name 
        self.child = child
        self.child.parent = self

    def view(self):
        return pn.pane.Markdown(f"I am {self.name}")

class StatefulSubcomponent(param.Parameterized):
    name = param.String()
    
    parent = param.Parameter()

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

    @param.depends('parent.name', 'name')
    def view(self):
        return pn.pane.Markdown(
            f'I am {self.name}. My parent is {self.parent.name}')


sub = StatefulSubcomponent(name='Chapter')
main = StatefulComponent(name='Book', child=sub)
pn.Column(
    main.view, 
    sub.view, 
).servable()

The other thing you seem to have missed is that for callbacks with side-effects you do not pass the callback to Panel, instead you declare watch=True in param.depends, i.e. instead of:

    @param.depends('name')
    def update_subcomponent(self):
        self.children.parents_name = self.name
...
pn.Column(
    main.view, 
    sub.view, 
    main.update_subcomponent # This is also ugly
).servable() 

you would simply do:

    @param.depends('name', watch=True)
    def update_subcomponent(self):
        self.children.parents_name = self.name
1 Like

Aah, so elegant! Thank you. This has grounded my understanding in several different areas.

2 Likes

@philippjfr, what if you want the method of a class to react to a parameter of a different class, but not a parent class? Is that possible?

1 Like

Yes. But how depends on your specific use case. An example is

import param

class A(param.Parameterized):
    value = param.Integer()

class B(param.Parameterized):
    a = param.ClassSelector(class_=A)
    value = param.Integer()

    @param.depends("a.value", watch=True)
    def _update_b_value(self):
        print("updating from", self.value)
        self.value += self.a.value
        print("updating to", self.value)

a=A()
b=B(a=a)

a.value=5
$ python 'script2.py'
updating from 0
updating to 5
1 Like

@Marc, thanks for the quick example of how to do this. It seems like the ClassSelector parameter is the key here and it helps to see it in use like this.

1 Like

I have a question on this topic. Is there anyway to use “a.value” param if I have defined a in the init of class B ? I have been getting NoneType issues
Link here

Like this?

import param

class A(param.Parameterized):
    value = param.Integer()

class B(param.Parameterized):
    a = param.Parameter()
    value = param.Integer()
    
    def __init__(self, **params):
        self.a = A()
        super().__init__(**params)
        

    @param.depends("a.value", watch=True)
    def _update_b_value(self):
        print("updating from", self.value)
        self.value += self.a.value
        print("updating to", self.value)

# Same functionality as Marc's example:
b=B()
b.a.value=5
1 Like

Hi, using param.ObjectSelector seems to work.

import param

class A(param.Parameterized):
    value = param.Integer()

class B(param.Parameterized):
    a_tmp=A()
    a = param.ObjectSelector(default=a_tmp,objects=[a_tmp])
    value = param.Integer()
    
    def __init__(self, **params):
        super().__init__(**params)
        

    @param.depends("a.value", watch=True)
    def _update_b_value(self):
        print("updating from", self.value)
        self.value += self.a.value
        print("updating to", self.value)

I defined a_tmp inside class B, because B.a uses a pointer to a_tmp. If you define it outside a class, all instances of B refer to the same instance of A().