holoviz/param

Cannot change instance attributes with a Callable without passing in parameterized class

Opened this issue ยท 3 comments

Problem:
Param callables do not work as expected given the purpose of the parametrized class.
Per the documentation, the purpose of the callables is partly to modify the attributes of the class.

Invocation parameters are a loose group of types that either contain an executable (callable) object, are invoked to execute some other code, or are set to change the value of some other parameter(s) or attribute(s).

Instead of having to pass self to the instance like so

class ExpEnv(param.Parameterized):
  url_input = param.String('test')
  url_list = param.List([])
  url_submit = param.Action(lambda self: self.url_list.append(self.url_input))

ee = ExpEnv()
ee.url_submit(ee)
ee.url_list

output: ['test']

it would be preferable to simply call the test_class.fn() where it implicitly passes self within the parameter creation like so:

ee.url_submit()

This makes more sense, given that the intent of the callable is to modify the attributes of the instance.

Additional context

Discourse discussion: https://discourse.holoviz.org/t/how-does-param-action-callable-handle-passing-self-to-function-unexpected-behavior-in-conjunction-with-panel/6665

Thanks for the good question! I've updated the issue title to indicate that it's not class attributes that are being updated, it's instance attributes, even though they are declared at the class level in the source code. (I.e. the action here updatesurl_list for the instance ee, not the class ExpEnv).

In any case, I agree that typing ee.url_submit() would be vastly more intuitive. How could we achieve that? ee.url_submit returns the actual function provided, which indeed does require the ee argument when called. Is the proposal to return a closure (a partial where self has already been supplied) that we create to wrap around the provided function, instead? I.e., effectively returning lambda: (lambda self: self.url_list.append(self.url_input)(ee) or functools.partial(lambda self: self.url_list.append(self.url_input), self=ee) rather than lambda self: self.url_list.append(self.url_input)? I'm not aware of any other case where we do that sort of doctoring to the actual value of the parameter when we look it up. Maybe it would be safe? Not sure.

An alternative could be to set it inside __init__, where self is available.

import param

class ExpEnv(param.Parameterized):
    url_input = param.String("test")
    url_list = param.List([])
    url_submit = param.Action()
    
    def __init__(self, **params):
        super().__init__(**params)
        self.url_submit = lambda *a: self.url_list.append(self.url_input) or self.param.trigger('url_list')


ee = ExpEnv()
ee.url_submit()

I have added *a to make it work with pn.panel(ee) and the or self.param.trigger('url_list') to signal that the url_list has been updated.

self.url_submit = lambda *a: self.url_list.append(self.url_input) or self.param.trigger('url_list')

That would make some code reviewers very unhappy ๐Ÿ˜„