Parameterized function

Background

All user-facing functions in this library should have built-in inputs validation. This is done by using the param library where

  • the inputs type can be validated without using 3rd-party interpreter like pypy.

  • bounds check are performed prior to the function execution.

  • corresponding front end can use the built-in type check to semi-automatically generate the ui code based off its sibling library, panel.

Usage example

Let’s assume that we need to convert the following generic Python function into a parameterized function:

def add(base: float, phase:float, exponent: int) -> float:
    return (base + phase) ** exponent

The very first step would be making the original function a private function by adding a leading underscore, which will prevent sphinx to generate the documentation for it.

def _add(base: float, phase:float, exponent: int) -> float:
    return (base + phase) ** exponent

Then, we can use the param.ParameterizedFunction to create a callable function as the user-facing wrapper:

import param

class add(param.ParameterizedFunction):
    """
    Callable functions demo

    Parameters
    ----------
    base : float
        The base value
    phase : float
        The phase value
    exponent : int
        The exponent value

    Returns
    -------
    float
        The result of the calculation
    """
    base = param.Number(default=0, bounds=(0, 1), doc="base value")
    phase = param.Number(default=0, bounds=(0, 1), doc="phase value")
    # default=None is required if the parameter doesn't have a default value
    exponent = param.Integer(default=None, bounds=(0, 10), doc="exponent value")

    def __call__(self, **params) -> float:
        # forced type+bounds check
        _ = self.instance(**params)
        # sanitize
        # NOTE: this will allow param to use default value if the input arg
        #       is missing from params.
        #       for example, if we call add(base=1, exponent=2), then param
        #       here will help fill in the missing arg, phase with its default
        #       i.e.  _add(base=1, phase=0, exponent=2)
        params = param.ParamOverrides(self, params)
        # call the actual function
        base = params.get("base")  # alternatively, params.base
        phase = params.get("phase")  # alternatively, params.phase
        exponent = params.get("exponent")  # alternatively, params.exponent
        return _add(base, phase, exponent)

Notice that the function docstring is added to the class docstring location with explicit type attention. This is because we want sphinx to render this as close as possible to the original function. The __call__ method is the only method that is required to be implemented, and it is possible to achieve polymorphism here if needed.

Unit tests

The original unit test function should be able cover the wrapper.

import pytest

def test_add():
    assert add(base=1.0, phase=0.2, exponent=2) == 1.44

However, if the logic inside __call__ is complicated, it is better to use unittest.mock to isolate logics and test the private functions independently. For more information, please refer to the unittest.mock documentation.

Generate widget from parameterized function

For simple function add, we can use the auto translation from panel to create a widget:

import panel as pn

pn.extension()
# auto translate input to widget
pn.Param(add.param)

However, complicated layout would still require the developer to extract the underlying parameters from the wrapped function, and manually create widget via

pn.widget.FloatSlider.from_param(add.param.base)

Tha main benefit here is that we can keep the type and bounds check in the backend and only use the widget to collect the user input.

Further reading

Please refer to the official param and panel documentation for more information.