Calculated properties: what is your opinion on the most param-y solution

Hi again!

I think this question corresponds to another slightly vague aspect of the documentation. Or rather, an area in which the documentation might be helpfully more opinionated about the best way to do something.

For many parameterized classes, there will be two sets of properties.

  1. parameters in the strict sense: properties that were supplied as arguments or defaults in the constructor
  2. calculated properties: properties whose values are generated by the constructor or by other methods, usually on the basis of (1)

(1) is clearly what param was made for. There are 3 ways I can think of for dealing with (2).

  • Code them as param.Parameters in the same way as properties of type (1), using constant=True to prevent them being set outside the class.
  • Code them as ordinary variables, using @param.depends, or a constructor, to recalculate or initialise them as needs be.
  • Finally, instead of making them variables, they could be written as methods. That seems a common approach in the docs.

So my question to those of you experienced in the ways of param is:

Which of these is the most in tune with param, or which do you prefer and why? Or what are the advantages and disadvantages of each?

Initially in my trying out param, I assumed the first would be the best because it imposed a tidy unity on the code. However, I’ve realised that it causes some problems, for instance, a panel.Param widget will include all the calculated properties.

e.g.:

class Triangle(param.Paramerterized):

    # properties defined as Parameters, 
    # but only the first two are parameters in the strict sense
    a = param.Number(2, bounds=(0, None))
    b = param.Number(3, bounds=(0, None))
    c = param.Number(bounds=(0, None), constant=True)

    @param.depends('a', 'b', watch=True)
    def _update_c(self):
        with param.edit_constant(self):
            self.c = a**2 + b**2

The second, which I’ve been trying out, is a bit more logical, but it seems to be mixing fish and foul. For instance, I don’t really feel comfortable with this without some typing, which doesn’t seem to be in the spirit of param:

class Triangle(param.Paramerterized):

    # parameters in the strict sense
    a = param.Number(2, bounds=(0, None))
    b = param.Number(3, bounds=(0, None))
 
    # a calculated property
    c: float

    @param.depends('a', 'b', watch=True)
    def _update_c(self):
        self.c = a**2 + b**2

Finally, this seems to be what the docs favour, but if the calculation is demanding I suppose it might cause performance issues…

class Triangle(param.Paramerterized):

    # parameters in the strict sense
    a = param.Number(2, bounds=(0, None))
    b = param.Number(3, bounds=(0, None))
 
    def c(self):
        return a**2 + b**2
1 Like

PS: I realise these don’t necessarily make possible triangles :sweat_smile:

A follow-up thought on this:

I’ve been trying something out using the second structure above, i.e. the one in which only parameters in the strict sense are parameterized and the other properties are just normal variables declared in the class. I’ve understood something about why I didn’t like this approach.

The advantage, as I said above, is that this works very simply with pn.Param to give a panel of widgets to set the parameters (narrow sense) of the class.

The disadvantage, from the point of view of doing things in the way param seems to expect, is that if any non-parameterised variables need to have a value when the object is instantiated, then the class must have a init method, which makes a mess of the otherwise beautifully elegant code.

Hey @JonathanMair, these are all very good questions and I think the answer is that there’s no specific API for computed properties (there could be one, maybe). The closest approach I can think of is your second solution, augmented with on_init=True to avoid having to add an __init__ to the class to compute the property value on instantiation:

class Triangle(param.Paramerterized):

    a = param.Number(2, bounds=(0, None))
    b = param.Number(3, bounds=(0, None))
    c = param.Number(bounds=(0, None), constant=True)

    @param.depends('a', 'b', watch=True, on_init=True)
    def _update_c(self):
        with param.edit_constant(self):
            self.c = a**2 + b**2

The drawback with this approach is that there’s quite some boilerplate code, the advantage being that it doesn’t break the reactivity, you can depend on/watch c from other objects if you want to. When I don’t need that, I believe I just resort to creating a normal Python property, having constrained the class input with well defined Parameters I generally don’t mind being loose on the “outputs”, YMMV:

class Triangle(param.Paramerterized):

    a = param.Number(2, bounds=(0, None))
    b = param.Number(3, bounds=(0, None))

    @property
    def c(self):
            return a**2 + b**2

I wonder how would like a new API dedicated to making computed properties easier to declare.

2 Likes

Thanks, @maximlt.

The reactivity was something I hadn’t thought about.

Also, the second solution loses the advantages of using slots for the parameters.

So perhaps the only drawbacks with the first solution are pretty minor: it’s less elegant and it means that panel.Param doesn’t work out of the box (but it’s pretty straightforward to customise).

I think I missed that question, to let Panel know that you don’t want a Parameter to be displayed you can set its precedence attribute to a negative value.

import param
import panel as pn

class Triangle(param.Paramerterized):

    a = param.Number(2, bounds=(0, None))
    b = param.Number(3, bounds=(0, None))
    c = param.Number(bounds=(0, None), constant=True, precedence=-1)

    @param.depends('a', 'b', watch=True, on_init=True)
    def _update_c(self):
        with param.edit_constant(self):
            self.c = a**2 + b**2

pn.panel(Triangle()).servable()

I’m not sure what you mean by that?

1 Like

Great!—thanks, that completely solves my problem. Now you mention it I think I even remember seeing it the docs or somewhere but had forgotten.

Now I know what to look for, it is clearly explained here: Param Precedence — Panel v1.3.5

Having a look at the docs again for panel.Param, I see the display_threshold setting is explained, and also the hide_constant setting, which could also be useful for dealing with this issue.

I’m not sure what you mean by that?

Sorry, ignore that, I was vaguely remembering something the docs say about the efficiency of using __slots__ instead of __dict__ but was getting confused between Parameterized and Parameter.

1 Like