How do you pass a parameter object (from param) to a child class and listen to changes there, and propagate the UI changes back up

I’m having some trouble figuring out how to use a more object oriented approach to panel and params. What it boils down to is that I have a dashboard, with a bunch of parameters on the left side, and several tabs on the right side.

+----+------+------+
| p1 | tab1 | tab2 | 
| p2 |------+------+
| p3 |   content   |
|    |             |
|    |             |
|    |             |
+----+-------------+

Ideally I want to split this into three classes:

  1. The parent class which contains the parameter pane on the left and organizes the tabs.
  2. A class for the first tab.
  3. A class for the second tab.

The idea is to prevent the creation of a big god class, by putting everything into smaller parameterized classes. To get this properly working I’m currently running into two issues, which are also present in the example code below:

  1. I’m not really sure how to pass a parameter instance to a different class and properly register listeners there. My initial guess would be to just add it to the param object, for example self.param.other_param = other_param in __init__, where other_param is passed as an argument. However this doesn’t seem to work.
  2. The child classes have a view() method that constructs the actual panel for that specific tab, however it looks like changes in this aren’t propagated/picked up by the parent class. While updating the return values of a method that has @param.depends shows up when it’s in the Parent class, these changes don’t show up in the UI when it’s in one of the Child classes.

It feels like I’m lacking some specific understanding of how panel/param works. I’d appreciate it if someone could point me in the right direction.

import panel as pn
import param

pn.extension()

class Child1(param.Parameterized):
    # Parameter that is only relevant to this class and shouldn't
    # have to be exposed to other classes.
    param3 = param.ObjectSelector(default="D", objects=["D", "E", "F"])
    
    def __init__(self, other_param):
        # My goal here'd be to be able to register `other_param` so I
        # can use it exactly the same as `param3` that's defined in
        # this class.
        #
        # For example:
        # self.param.other_param = other_param  # ?
        
        super().__init__()
    
    @param.depends("param3", "other_param")
    def _text(self):
        # This method should also be called when `other_param` is
        # changed so we can change the text 
        return f"Param3: {self.param3} -- placeholder"
    
    def view(self):
        # This method should construct the entire view for the tab
        # that this class is responsible for. 
        return pn.Column(pn.Param(self.param), self._text())
    
class Child2(param.Parameterized):
    
    param4 = param.ObjectSelector("4", objects=["4", "5", "6"])
    param5 = param.ObjectSelector("7", objects=["7", "8", "9"])

    def __init__(self, other_param1, other_param2):
        # Basically all the same comments apply here as in the
        # `Child1` class.
        super().__init__()
    
    @param.depends("param4", "param5", "other_param1", "other_param2")
    def _text(self):
        return f"{self.param4} -- {self.param5} -- placeholder -- placeholder"
    
    def view(self):
        return pn.Column(pn.Param(self.param), self._text())
    

class Parent(param.Parameterized):
    # These two parameters are defined in the parent class as they're
    # used as input for various child classes.
    param1 = param.ObjectSelector(default="A", objects=["A", "B", "C"])
    param2 = param.ObjectSelector(default="1", objects=["1", "2", "3"])
    
    def __init__(self):
        # Depending on what parameters the child class needs I'd like
        # to be able to pass the entire parameter object, so that these
        # child classes are also able to listen to changes.
        self.child1 = Child1(self.param.param1)
        self.child2 = Child2(self.param.param1, self.param.param2)

        super().__init__()
        
    @param.depends("param1", "param2")
    def _text(self):
        return f"Params: {self.param1} -- {self.param2}"
    
    def view(self):
        return pn.Row(
            pn.Column(pn.Param(self.param), self._text),
            pn.Tabs(
                ("Child1", self.child1.view()),
                ("Child2", self.child2.view())   
            )
        )
    
Parent().view()

Alright, so somewhat of an update.

  1. Changes in the view of the child classes are now also shown in the UI. I accidentally used self._text() instead of self._text in the view() methods.
  2. I’ve found a way to listen to parameter changes in the parent. Unfortunately this is by passing the entire parent to the child classes. At that point I’m able to listen to parameters like parent.param1 or parent.param2.

My goal is still to just pass single parameter objects like ObjectSelector, instead of the entire parameterized class. If anyone has a suggestion I’d love to hear it.

For now I’ve put the updated code below.

import panel as pn
import param

pn.extension()

class Child1(param.Parameterized):
    # Parameter that is only relevant to this class and shouldn't
    # have to be exposed to other classes.
    param3 = param.ObjectSelector(default="D", objects=["D", "E", "F"])
    
    def __init__(self, parent):
        # My goal here'd be to be able to register an actual param so I
        # can use it exactly the same as `param3` that's defined in
        # this class. At this point I'm passing the entire parent object,
        # and while it works, it's more than I'd like. 
        #
        # For example:
        self.parent = parent
        super().__init__()
    
    @param.depends("param3", "parent.param1")
    def _text(self):
        # Currently this method is called when parent.param1 changes,
        # however I'd rather be able to pass `param1` in the constructor
        # so I don't need the entire parent object here.
        return f"{self.param3} -- {self.parent.param1}"
    
    def view(self):
        # This method should construct the entire view for the tab
        # that this class is responsible for. 
        return pn.Column(pn.Param(self.param), self._text)
    
class Child2(param.Parameterized):
    
    param4 = param.ObjectSelector("4", objects=["4", "5", "6"])
    param5 = param.ObjectSelector("7", objects=["7", "8", "9"])

    def __init__(self, parent):
        # Basically all the same comments apply here as in the
        # `Child1` class.
        self.parent = parent
        super().__init__()
    
    @param.depends("param4", "param5", "parent.param1", "parent.param2")
    def _text(self):
        return f"{self.param4} -- {self.param5} -- {self.parent.param1} -- {self.parent.param2}"
    
    def view(self):
        return pn.Column(pn.Param(self.param), self._text)


class Parent(param.Parameterized):
    # These two parameters are defined in the parent class as they're
    # used as input for various child classes.
    param1 = param.ObjectSelector(default="A", objects=["A", "B", "C"])
    param2 = param.ObjectSelector(default="1", objects=["1", "2", "3"])
    
    def __init__(self):
        # Depending on what parameters the child class needs I'd like
        # to be able to pass the entire parameter object, so that these
        # child classes are also able to listen to changes.

        super().__init__()
        self.child1 = Child1(self)
        self.child2 = Child2(self)
        
    @param.depends("param1", "param2")
    def _text(self):
        return f"Params: {self.param1} -- {self.param2}"
    
    def view(self):
        return pn.Row(
            pn.Column(pn.Param(self.param), self._text),
            pn.Tabs(
                ("Child1", self.child1.view()),
                ("Child2", self.child2.view())   
            )
        )
    
Parent().view()